@bongos/core 1.21.167 → 1.21.169

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 (83) hide show
  1. package/.bongos-core.json +157 -72
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +8 -0
  4. package/clients/bongos-client/index.cjs +8 -0
  5. package/clients/bongos-client/index.d.ts +13 -0
  6. package/clients/bongos-client/index.mjs +8 -0
  7. package/docs/api/openapi.json +308 -5
  8. package/docs/api-reference.md +7 -3
  9. package/docs/architecture.md +2 -0
  10. package/docs/copy-inventory.md +21 -20
  11. package/docs/copy-registry.json +34 -25
  12. package/docs/module-api-changelog.md +5 -1
  13. package/docs/modules-contract.md +2 -1
  14. package/docs/page-inventory.json +3 -0
  15. package/docs/page-readings.json +430 -430
  16. package/modules/autonomy/governor-docket.js +10 -5
  17. package/modules/builder-settings/builder-needs.js +29 -3
  18. package/modules/copy-desk/guarded-lines.js +89 -0
  19. package/modules/copy-desk/guarded-lines.json +14 -0
  20. package/modules/copy-desk/module.json +2 -2
  21. package/modules/copy-desk/page-status.js +77 -7
  22. package/modules/copy-desk/pages.js +4 -0
  23. package/modules/copy-desk/pollers/stuck-page-digest.js +34 -0
  24. package/modules/copy-desk/routes/copy-desk.js +8 -0
  25. package/modules/copy-desk/stuck-tweaks.js +79 -0
  26. package/modules/copy-desk/tests/copy_no_cms.mjs +6 -1
  27. package/modules/copy-desk/tests/copy_page_status.mjs +2 -2
  28. package/modules/government/asks-db.js +38 -2
  29. package/modules/government/catalog.js +1 -1
  30. package/modules/government/docket.js +20 -2
  31. package/modules/government/migrations/government_032_ask_flags.sql +35 -0
  32. package/modules/government/routes/government.js +43 -1
  33. package/modules/hall-ui/public/approval-queue-lib.js +4 -0
  34. package/modules/hall-ui/public/approval-queue.js +1 -0
  35. package/modules/hall-ui/public/city-decide.js +642 -0
  36. package/modules/hall-ui/public/city-draw.js +14 -3
  37. package/modules/hall-ui/public/city-switchboard.js +141 -0
  38. package/modules/hall-ui/public/city.css +176 -10
  39. package/modules/hall-ui/public/city.html +8 -3
  40. package/modules/hall-ui/public/city.js +185 -38
  41. package/modules/hall-ui/public/tweak-editor-lib.js +21 -2
  42. package/modules/hall-ui/public/tweak-editor.css +2 -0
  43. package/modules/hall-ui/public/tweak-editor.js +3 -1
  44. package/modules/lifecycle/db-claims.js +20 -5
  45. package/modules/lifecycle/rebase-gate.js +78 -0
  46. package/modules/lifecycle/routes/claims.js +10 -2
  47. package/package-lock.json +2 -2
  48. package/package.json +1 -1
  49. package/release-notes.json +28 -0
  50. package/scripts/gds/ask.js +68 -1
  51. package/scripts/gds/claim.js +6 -1
  52. package/scripts/gds/copy-apply.js +2 -0
  53. package/src/bongos/module-promotion.js +219 -0
  54. package/src/bongos/module-store.js +47 -9
  55. package/src/bongos/route-rank-check.js +7 -0
  56. package/src/bongos/routes/me.js +11 -0
  57. package/src/bongos/routes/modules.js +72 -10
  58. package/src/module-api.js +1 -1
  59. package/src/platform-server.js +5 -0
  60. package/tests/claim_gate_ci_unblock.mjs +4 -0
  61. package/tests/claim_gate_rebase.mjs +56 -3
  62. package/tests/copy_desk_page_editor.mjs +1 -1
  63. package/tests/copy_desk_page_submit.mjs +42 -0
  64. package/tests/copy_guarded_lines.mjs +54 -0
  65. package/tests/copy_stuck_page_tweaks.mjs +118 -0
  66. package/tests/government_ask_flags.mjs +80 -0
  67. package/tests/government_seed.mjs +1 -0
  68. package/tests/government_treasury.mjs +1 -0
  69. package/tests/governor_docket.mjs +1 -1
  70. package/tests/hall_approval_queue.mjs +15 -0
  71. package/tests/hall_city.mjs +5 -4
  72. package/tests/hall_city_living.mjs +118 -12
  73. package/tests/hall_city_rewording.mjs +51 -0
  74. package/tests/hall_city_switchboard.mjs +63 -0
  75. package/tests/hall_gov_real_site.mjs +2 -2
  76. package/tests/hall_street_deck.mjs +6 -4
  77. package/tests/hall_street_depth.mjs +1 -1
  78. package/tests/hall_street_desk.mjs +5 -3
  79. package/tests/hall_tweak_editor.mjs +26 -0
  80. package/tests/module_channel_install.mjs +112 -0
  81. package/tests/module_promotion.mjs +290 -0
  82. package/tests/module_upgrade_updates.mjs +2 -2
  83. package/tests/rebase_gate_set_aside.mjs +73 -0
@@ -532,6 +532,23 @@ function registeredContributions() {
532
532
  // the matters this viewer already sees, so an ask is never more visible than its
533
533
  // matter. A read that fails or is slow attaches none: the city still stands, and
534
534
  // each matter keeps its own words.
535
+ //
536
+ // The reword flags (government_032, owner 2026-10-23) ride the same way: a
537
+ // matter someone pressed "?" on carries `unclear` { flagged_at, note, times }.
538
+ async function attachFlags(matters, readFlags, timeoutMs) {
539
+ if (typeof readFlags !== 'function' || !matters.length) return;
540
+ try {
541
+ const found = await withTimeout(Promise.resolve(readFlags(matters.map((m) => m.id))), timeoutMs);
542
+ if (!found || typeof found.get !== 'function') return;
543
+ for (const m of matters) {
544
+ const flag = found.get(m.id);
545
+ if (flag) m.unclear = flag;
546
+ }
547
+ } catch (err) {
548
+ log.warn({ err: err && err.message }, 'governor docket: reword flags unreadable');
549
+ }
550
+ }
551
+
535
552
  async function attachAsks(matters, readAsks, timeoutMs) {
536
553
  if (typeof readAsks !== 'function' || !matters.length) return;
537
554
  try {
@@ -561,7 +578,7 @@ async function attachAsks(matters, readAsks, timeoutMs) {
561
578
  // `seats` is who sits in each seat (loadSeats below: Leads in office, the owner
562
579
  // seat, each building's holders); given, every matter carries `route` (routing.js,
563
580
  // task 1004705) and the docket says `routed: true`. null routes nothing.
564
- async function buildDocket({ viewer, permissions, contributions = registeredContributions(), now = Date.now(), timeoutMs = SOURCE_TIMEOUT_MS, decisions = null, government = null, readAsks = null, seats = null } = {}) {
581
+ async function buildDocket({ viewer, permissions, contributions = registeredContributions(), now = Date.now(), timeoutMs = SOURCE_TIMEOUT_MS, decisions = null, government = null, readAsks = null, readFlags = null, seats = null } = {}) {
565
582
  const held = new Set(permissions || []);
566
583
  const ctx = Object.freeze({
567
584
  builderId: String(viewer.id),
@@ -614,6 +631,7 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
614
631
  sourceStatus.sort((a, b) => order.indexOf(a.source) - order.indexOf(b.source));
615
632
  matters.sort(compareMatters);
616
633
  await attachAsks(matters, readAsks, timeoutMs);
634
+ await attachFlags(matters, readFlags, timeoutMs);
617
635
  // THE DIRECTION DESK (task 1004705): who each matter waits on, from real seats.
618
636
  const yours = seats ? routing.routeAll(matters, ctx.builderId, seats, now) : null;
619
637
 
@@ -813,7 +831,7 @@ async function docketFor(builder) {
813
831
  });
814
832
  const government = permissions.includes(GOVERNMENT_GATE.permission) ? await loadGovernment({ decisions }) : null;
815
833
  const seats = government ? await loadSeats(government) : null;
816
- return buildDocket({ viewer: builder, permissions, decisions, government, readAsks: asksDb.asksFor, seats });
834
+ return buildDocket({ viewer: builder, permissions, decisions, government, readAsks: asksDb.asksFor, readFlags: asksDb.unclearFor, seats });
817
835
  }
818
836
 
819
837
  // Who sits in each seat now (task 1004705), read per request, uncached (ADR
@@ -0,0 +1,35 @@
1
+ -- government_032_ask_flags.sql — THE REWORD LIST: a reader says a card does not
2
+ -- make sense (owner, 2026-10-23: the "?" on every city card).
3
+ --
4
+ -- WHAT THIS ADDS
5
+ --
6
+ -- government_ask_flags — ONE ROW PER DOCKET MATTER someone flagged as unclear,
7
+ -- keyed by the matter's docket id (`blockers:12`), the same key as
8
+ -- government_asks. An AI session works the open rows (GET
9
+ -- /government/asks/unclear), writes the matter a proper ask (PUT
10
+ -- /government/asks/:matter, the standard short card), and that write closes
11
+ -- the flag (resolved_at). Flagging again after a rewrite reopens it.
12
+ --
13
+ -- A flag is a request, not a verdict: it grants nothing and changes nothing a
14
+ -- person decides. `note` is the reader's optional words on what confused them.
15
+ --
16
+ -- Additive and idempotent: a new table (IF NOT EXISTS) and one index. No row is
17
+ -- seeded.
18
+
19
+ BEGIN;
20
+
21
+ CREATE TABLE IF NOT EXISTS government_ask_flags (
22
+ matter_id text PRIMARY KEY
23
+ CHECK (matter_id ~ '^[a-z][a-z_]{1,39}:[A-Za-z0-9._-]{1,64}$'),
24
+ flagged_by bigint NULL,
25
+ flagged_at timestamptz NOT NULL DEFAULT now(),
26
+ note text NULL CHECK (note IS NULL OR char_length(note) <= 500),
27
+ times integer NOT NULL DEFAULT 1 CHECK (times >= 1),
28
+ resolved_at timestamptz NULL
29
+ );
30
+
31
+ -- the open reword list, oldest first
32
+ CREATE INDEX IF NOT EXISTS government_ask_flags_open_idx
33
+ ON government_ask_flags (flagged_at) WHERE resolved_at IS NULL;
34
+
35
+ COMMIT;
@@ -826,6 +826,48 @@ module.exports = function buildGovernmentRouter() {
826
826
  }
827
827
  });
828
828
 
829
+ // rank: perm board.vote.cast — "I DON'T UNDERSTAND THIS" (the "?" on a city
830
+ // card, owner 2026-10-23): puts a matter the flagger can see on the reword
831
+ // list (government_032). An AI session works that list and writes the matter
832
+ // a standard ask (PUT above), which closes the flag. A flag changes nothing a
833
+ // person decides; it only asks for better words.
834
+ //
835
+ // Body: { note? } (≤ 500 chars, what confused them) → 200 { ok, matter_id, flag }
836
+ router.post('/government/asks/:matterId/unclear', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
837
+ const matterId = String(req.params.matterId || '');
838
+ if (!asks.isMatterId(matterId)) return res.fail('bad_matter_id', { status: 400, message: 'Name the matter by its docket id, like blockers:12.' });
839
+ if (validateOrRespond(req, res, { note: { type: 'string', maxLength: 500 } })) return;
840
+ try {
841
+ const found = await docket.findMatterFor(req.builder, matterId);
842
+ if (!found.matter) {
843
+ if (found.reason === 'unavailable') return res.fail('matter_unavailable', { status: 503, message: 'That matter could not be read just now, so nothing was flagged. Try again.' });
844
+ return res.fail('matter_not_found', { status: 404, message: 'No open matter by that id that you can see.' });
845
+ }
846
+ const flag = await asksDb.flagUnclear({ matterId, builderId: req.builder.id, note: req.body && req.body.note });
847
+ res.json({ ok: true, matter_id: matterId, flag });
848
+ } catch (err) {
849
+ log.error('[gds] POST /government/asks/:matterId/unclear', err);
850
+ res.fail('flag_save_failed', { status: 500, message: 'internal error' });
851
+ }
852
+ });
853
+
854
+ // rank: perm board.vote.cast — THE REWORD LIST: the open matters this viewer
855
+ // can see that someone flagged as unclear, oldest flag first, each with its
856
+ // words as they stand (title, detail, stored ask) so a session can rewrite it.
857
+ // → 200 { ok, matters: [{ id, building, source, title, detail, ask, flag }] }
858
+ router.get('/government/asks/unclear', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
859
+ try {
860
+ const d = await docket.docketFor(req.builder);
861
+ const list = (d.matters || []).filter((m) => m.unclear)
862
+ .sort((a, b) => String(a.unclear.flagged_at).localeCompare(String(b.unclear.flagged_at)))
863
+ .map((m) => ({ id: m.id, building: m.building, source: m.source, title: m.title, detail: m.detail || null, ask: m.ask || null, flag: m.unclear }));
864
+ res.json({ ok: true, matters: list });
865
+ } catch (err) {
866
+ log.error('[gds] GET /government/asks/unclear', err);
867
+ res.fail('flags_read_failed', { status: 500, message: 'internal error' });
868
+ }
869
+ });
870
+
829
871
  // rank: perm board.vote.cast — PASS AN ASK ON to another seat (task 1004705,
830
872
  // the direction desk). Only whoever the ask waits on today may hand it on
831
873
  // (routing.passRefusal, from real seats: its Lead, the owner seat, a board's
@@ -847,7 +889,7 @@ module.exports = function buildGovernmentRouter() {
847
889
  }
848
890
  const refused = routing.passRefusal(found.matter, req.builder.id, seat, found.seats);
849
891
  if (refused) return res.fail(refused.code, { status: refused.status, message: refused.message });
850
- const ask = await asksDb.saveAsk({ matterId, ask: { ...found.matter.ask, waits_on: seat }, builderId: req.builder.id });
892
+ const ask = await asksDb.saveAsk({ matterId, ask: { ...found.matter.ask, waits_on: seat }, builderId: req.builder.id, resolveFlag: false });
851
893
  const route = routing.routeMatter({ ...found.matter, ask }, req.builder.id, found.seats);
852
894
  res.json({ ok: true, matter_id: matterId, ask, route });
853
895
  } catch (err) {
@@ -120,6 +120,10 @@
120
120
  aside: line.refused ? refusedWords(line.refused) : (after ? `was: ${line.before}` : `becomes: ${line.after}`),
121
121
  refused: !!line.refused,
122
122
  // task 1004775: a test still asserts the old words, so it fails once this lands.
123
+ // task 1004776: the rewrite dropped what this line must still say.
124
+ ...(!line.refused && line.must_still_say
125
+ ? { guarded: `This line no longer says what it must: ${line.must_still_say}` }
126
+ : {}),
123
127
  ...(!line.refused && Array.isArray(line.pinned_by) && line.pinned_by.length
124
128
  ? { pinned: `A test still checks the old words: ${line.pinned_by.join(', ')}. It needs updating when this ships.` }
125
129
  : {}),
@@ -152,6 +152,7 @@
152
152
  ${r.section ? `<span class="aq-line__section">${esc(r.section)}</span>` : ''}
153
153
  <span class="aq-line__text">${r.marked ? `<mark class="aq-mark">${words(r.text)}</mark>` : words(r.text)}</span>
154
154
  <span class="aq-line__aside">${words(r.aside)}</span>
155
+ ${r.guarded ? `<span class="aq-line__pinned">${esc(r.guarded)}</span>` : ''}
155
156
  ${r.pinned ? `<span class="aq-line__pinned">${esc(r.pinned)}</span>` : ''}
156
157
  </li>`).join('')}</ol>`;
157
158
  }