@bongos/core 1.21.39 → 1.21.41

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 (88) hide show
  1. package/.bongos-core.json +144 -84
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +16 -0
  4. package/clients/bongos-client/index.cjs +16 -0
  5. package/clients/bongos-client/index.d.ts +25 -0
  6. package/clients/bongos-client/index.mjs +16 -0
  7. package/docs/api/openapi.json +492 -3
  8. package/docs/api-reference.md +13 -1
  9. package/docs/copy-inventory.md +29 -13
  10. package/docs/copy-registry.json +177 -23
  11. package/docs/file-map.md +1 -0
  12. package/docs/module-api-changelog.md +4 -0
  13. package/docs/page-inventory.json +14 -2
  14. package/docs/page-readings.json +601 -554
  15. package/modules/government/docket.js +14 -0
  16. package/modules/hall-ui/public/atlas.html +3 -3
  17. package/modules/hall-ui/public/blockers.html +3 -3
  18. package/modules/hall-ui/public/board-room.html +3 -3
  19. package/modules/hall-ui/public/city.html +3 -3
  20. package/modules/hall-ui/public/collab.html +3 -3
  21. package/modules/hall-ui/public/commands.html +3 -3
  22. package/modules/hall-ui/public/copy-desk.html +3 -3
  23. package/modules/hall-ui/public/craft-mode.js +62 -0
  24. package/modules/hall-ui/public/deploy.html +3 -3
  25. package/modules/hall-ui/public/diagrams.html +3 -3
  26. package/modules/hall-ui/public/drachmae.html +3 -3
  27. package/modules/hall-ui/public/fleet.html +3 -3
  28. package/modules/hall-ui/public/gate.html +3 -3
  29. package/modules/hall-ui/public/goal-map.html +3 -3
  30. package/modules/hall-ui/public/goals.html +3 -3
  31. package/modules/hall-ui/public/government.html +3 -3
  32. package/modules/hall-ui/public/idea.html +3 -3
  33. package/modules/hall-ui/public/ideas.html +3 -3
  34. package/modules/hall-ui/public/index.html +3 -3
  35. package/modules/hall-ui/public/modules.html +3 -3
  36. package/modules/hall-ui/public/page-notes.js +320 -0
  37. package/modules/hall-ui/public/primer.html +3 -3
  38. package/modules/hall-ui/public/profile.html +3 -3
  39. package/modules/hall-ui/public/project-settings.html +3 -3
  40. package/modules/hall-ui/public/ranks.html +3 -3
  41. package/modules/hall-ui/public/roadmap.html +3 -3
  42. package/modules/hall-ui/public/roster.html +3 -3
  43. package/modules/hall-ui/public/sessions.html +3 -3
  44. package/modules/hall-ui/public/settings.html +3 -3
  45. package/modules/hall-ui/public/street-card.css +21 -2
  46. package/modules/hall-ui/public/street-card.js +198 -64
  47. package/modules/hall-ui/public/street-card.states.json +495 -7
  48. package/modules/hall-ui/public/street-desk.js +47 -2
  49. package/modules/hall-ui/public/street.css +1 -3
  50. package/modules/hall-ui/public/street.html +3 -4
  51. package/modules/hall-ui/public/street.js +42 -15
  52. package/modules/hall-ui/public/studio.html +3 -3
  53. package/modules/hall-ui/public/style.css +144 -0
  54. package/modules/hall-ui/public/task.html +3 -3
  55. package/modules/hall-ui/public/thinking.html +3 -3
  56. package/modules/hall-ui/public/tweak-editor.html +3 -3
  57. package/modules/hall-ui/public/watch.html +3 -3
  58. package/modules/hall-ui/public/work.html +3 -3
  59. package/modules/hall-ui/records/page-notes.md +16 -0
  60. package/modules/ideas/governor-docket.js +2 -0
  61. package/modules/lifecycle/governor-docket.js +4 -0
  62. package/modules/onboarding/routes/access-requests.js +2 -0
  63. package/modules/page-notes/CLAUDE.md +15 -0
  64. package/modules/page-notes/db.js +147 -0
  65. package/modules/page-notes/migrations/page_notes_001_notes.sql +68 -0
  66. package/modules/page-notes/module.json +13 -0
  67. package/modules/page-notes/notes.js +174 -0
  68. package/modules/page-notes/routes/page-notes.js +172 -0
  69. package/modules/page-notes/split.js +68 -0
  70. package/modules/provisioning/starter-bundles.js +1 -0
  71. package/modules/security/routes/reports.js +3 -0
  72. package/modules/ui-design/kit/serve.js +1 -1
  73. package/package-lock.json +2 -2
  74. package/package.json +1 -1
  75. package/release-notes.json +12 -0
  76. package/scripts/gds/publish-manifest.js +1 -0
  77. package/src/bongos/module-rechecks.js +12 -1
  78. package/src/bongos/module-scope-map.js +4 -0
  79. package/src/bongos/routes.js +4 -1
  80. package/src/module-api.js +1 -1
  81. package/tests/governor_docket.mjs +20 -0
  82. package/tests/hall_matter_card.mjs +143 -18
  83. package/tests/hall_street.mjs +1 -1
  84. package/tests/hall_tweak_editor.mjs +1 -1
  85. package/tests/module_loader.mjs +1 -1
  86. package/tests/page_notes.mjs +289 -0
  87. package/tests/page_notes_db.mjs +140 -0
  88. package/tests/page_notes_panel.mjs +145 -0
@@ -78,7 +78,9 @@
78
78
  var $ = function (id) { return document.getElementById(id); };
79
79
  var media = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
80
80
  var data = { docket: null, goals: [], rollups: [], earnings: null, loaded: false, tasks: {}, room: null, constitution: null };
81
- var card = { cv: null, form: null, error: '', busy: false, fresh: null, answer: null };
81
+ // ask: the act whose chips stand open; more: the detail is open in place;
82
+ // pick: which of the review queue the queue's card shows (task 1004566)
83
+ var card = { cv: null, form: null, ask: null, more: false, pick: null, error: '', busy: false, fresh: null, answer: null };
82
84
  var view = { open: null, opener: null, model: null, scale: 1, mode: null, inside: null, insideModel: null };
83
85
  // the desk's own state: rows you acted on this visit, what you did per floor,
84
86
  // the floors you cleared, the row whose facts or chips are open
@@ -1041,21 +1043,42 @@
1041
1043
 
1042
1044
 
1043
1045
  // ---------- the matter card (task 1004528: it acts) ----------
1046
+ // Its acts are the desk row's for the same item (GovDesk.cardFor, task
1047
+ // 1004566): the same words, chips, routes and permission checks. "more" grows
1048
+ // the card in place; nothing on it opens an older page to decide.
1044
1049
  function fillCard() {
1045
- var C = window.GovMatterCard;
1046
- var cv = C ? C.model({ docket: data.docket, key: view.open, room: data.room, constitution: data.constitution, now: Date.now() }) : null;
1050
+ var C = window.GovMatterCard, D = window.GovDesk;
1051
+ var ctx = { room: data.room, constitution: data.constitution, now: Date.now(), goals: (data.goals || []).filter(function (g) { return g && g.status === 'open'; }), pick: card.pick };
1052
+ var cv = D && D.cardFor ? D.cardFor(view.open, data.docket, ctx) : C ? C.model({ docket: data.docket, key: view.open, room: data.room, constitution: data.constitution, now: Date.now() }) : null;
1047
1053
  if (!cv) { closeCard(false); return null; }
1048
1054
  card.cv = cv;
1049
1055
  $('streetCardWhere').textContent = cv.where;
1050
1056
  $('streetCardWhen').textContent = cv.when;
1051
1057
  $('streetCardTitle').textContent = cv.title;
1052
- C.render(document, $('streetCardBody'), cv, { act: actOn, openForm: openForm }, { form: card.form, error: card.error, busy: card.busy, fresh: card.fresh });
1053
- var open = $('streetCardOpen');
1054
- if (cv.href) open.setAttribute('href', cv.href);
1055
- open.hidden = !cv.href;
1058
+ C.render(document, $('streetCardBody'), cv, { act: actOn, openForm: openForm, ask: askFor, more: toggleMore, pick: pickQueued }, { form: card.form, ask: card.ask, more: card.more, error: card.error, busy: card.busy, fresh: card.fresh });
1059
+ var frame = $('streetCard'); if (frame) frame.setAttribute('data-more', card.more ? 'open' : 'closed');
1056
1060
  placeCard();
1057
1061
  return cv;
1058
1062
  }
1063
+ function askFor(key) {
1064
+ card.ask = card.ask === key ? null : key;
1065
+ card.error = '';
1066
+ fillCard();
1067
+ var body = $('streetCardBody'), first = body && body.querySelector ? body.querySelector('.mcard-chip, .mcard-chips__field') : null;
1068
+ if (card.ask && first && first.focus) first.focus();
1069
+ }
1070
+ function toggleMore() {
1071
+ card.more = !card.more;
1072
+ fillCard();
1073
+ var mb = $('streetCardBody'), t = mb && mb.querySelector ? mb.querySelector('[data-act="more"]') : null;
1074
+ if (t && t.focus) t.focus();
1075
+ }
1076
+ function pickQueued(key) {
1077
+ card.pick = key; card.ask = null; card.more = false; card.form = null; card.error = '';
1078
+ fillCard();
1079
+ var close = $('streetCardClose');
1080
+ if (close && close.focus) close.focus();
1081
+ }
1059
1082
  function placeCard() {
1060
1083
  var card = $('streetCard');
1061
1084
  if (!card || !view.open || !view.model) return;
@@ -1071,7 +1094,7 @@
1071
1094
  function openCard(key, opener) {
1072
1095
  view.open = key;
1073
1096
  view.opener = opener || null;
1074
- card.form = null; card.error = ''; card.fresh = null;
1097
+ card.form = null; card.ask = null; card.more = false; card.pick = null; card.error = ''; card.fresh = null;
1075
1098
  var el = $('streetCard');
1076
1099
  if (!fillCard()) return;
1077
1100
  el.hidden = false;
@@ -1084,7 +1107,7 @@
1084
1107
  var was = view.opener;
1085
1108
  view.open = null;
1086
1109
  view.opener = null;
1087
- card.cv = null; card.form = null; card.error = ''; card.busy = false; card.fresh = null;
1110
+ card.cv = null; card.form = null; card.ask = null; card.more = false; card.pick = null; card.error = ''; card.busy = false; card.fresh = null;
1088
1111
  var el = $('streetCard');
1089
1112
  if (el) el.hidden = true;
1090
1113
  draw();
@@ -1123,32 +1146,36 @@
1123
1146
  card.form = name || null;
1124
1147
  card.error = '';
1125
1148
  fillCard();
1126
- var f = name === 'object' ? $('mcardReason') : name === 'revise' ? $('mcardRevise') : $('streetCardClose');
1149
+ var f = name === 'revise' ? $('mcardRevise') : $('streetCardClose');
1127
1150
  if (f && f.focus) f.focus();
1128
1151
  }
1129
1152
  // One act: checked, sent to its own route (the server's permission checks
1130
1153
  // decide), then answered. An act that records nothing (abstain, hold) is
1131
1154
  // answered without a request.
1155
+ // An act from the card goes with the desk row's own card when a row stands
1156
+ // behind it (its subject: which route, the council's), else the model's.
1132
1157
  function actOn(key, input) {
1133
1158
  var C = window.GovMatterCard, cv = card.cv;
1134
1159
  if (!C || !cv || card.busy) return Promise.resolve(false);
1135
- var refused = C.check(cv, key, input);
1160
+ var subj = cv.row && cv.row.card ? cv.row.card : cv;
1161
+ var refused = C.check(subj, key, input);
1136
1162
  if (refused) { card.error = refused; fillCard(); return Promise.resolve(false); }
1137
1163
  card.error = '';
1138
- var req = C.request(cv, key, input);
1139
- if (!req) return Promise.resolve(answered(cv, key, {}, false));
1164
+ var req = C.request(subj, key, input);
1165
+ if (!req) return Promise.resolve(answered(subj, key, {}, false));
1140
1166
  card.busy = true;
1141
1167
  fillCard();
1142
1168
  return send(req).then(function (out) {
1143
1169
  card.busy = false;
1144
1170
  if (!out.ok) { card.error = C.refusal(out.status, out.body); if (view.open) fillCard(); return false; }
1145
- return answered(cv, key, out.body, true);
1171
+ return answered(subj, key, out.body, true);
1146
1172
  });
1147
1173
  }
1148
1174
  function answered(cv, key, body, recorded) {
1149
1175
  var a = window.GovMatterCard.answer(cv, key, body);
1150
1176
  say(a.toast);
1151
- card.form = null;
1177
+ card.form = null; card.ask = null;
1178
+ card.pick = null;
1152
1179
  card.fresh = key === 'for' || key === 'object' ? 'you' : null;
1153
1180
  if (a.building && a.kind) card.answer = { building: a.building, matter: cv.key, kind: a.kind, at: Date.now() };
1154
1181
  if (a.close) closeCard(true);
@@ -176,9 +176,9 @@
176
176
 
177
177
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
178
178
 
179
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
180
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
181
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
179
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
180
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
181
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
182
182
  <script src="/builders/palette.js" defer></script>
183
183
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
184
184
  <!-- The kit, for its ONE renderer: the full-size ship-time visual figure
@@ -1550,6 +1550,150 @@ html[data-theme="light"][data-craft="ideating"] .app-sidebar {
1550
1550
  radial-gradient(1.5px 1.5px at 170px 260px, rgba(184, 137, 28, .25), transparent);
1551
1551
  }
1552
1552
 
1553
+ /* THE MODE TOOLS (task 1004496, craft-mode.js TOOLS): a round button beside the
1554
+ switch, in the mode's colour, shown only in its own mode. Artistry's palette
1555
+ opens the page feedback doc (page-notes.js). */
1556
+ .craft-tool {
1557
+ display: inline-flex;
1558
+ align-items: center;
1559
+ justify-content: center;
1560
+ width: 32px; height: 32px;
1561
+ padding: 0;
1562
+ border: 0;
1563
+ border-radius: var(--radius-pill);
1564
+ background: none;
1565
+ color: var(--craft, var(--ink-soft));
1566
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--craft, var(--ink)) 40%, transparent);
1567
+ cursor: pointer;
1568
+ flex-shrink: 0;
1569
+ }
1570
+ .craft-tool:hover { background: color-mix(in srgb, var(--craft, var(--ink)) 12%, transparent); }
1571
+ .craft-tool:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
1572
+ .craft-tool[aria-disabled="true"] { opacity: .45; cursor: default; }
1573
+ .craft-tool__icon { width: 16px; height: 16px; flex-shrink: 0; }
1574
+ /* With room, the tool says what it does, so it cannot be read as the mode's own
1575
+ pill (same palette glyph); under 1180px it folds to the icon, as the switch does. */
1576
+ .craft-tool__label { font-size: 13px; white-space: nowrap; }
1577
+ @media (min-width: 1181px) {
1578
+ .craft-tool { width: auto; gap: 6px; padding: 0 12px 0 10px; }
1579
+ }
1580
+ @media (max-width: 1180px) { .craft-tool__label { display: none; } }
1581
+
1582
+ /* The page feedback doc: a sheet from the right over a scrim. */
1583
+ html.pn-open { overflow: hidden; }
1584
+ .pn {
1585
+ position: fixed;
1586
+ inset: 0;
1587
+ z-index: var(--z-modal);
1588
+ display: flex;
1589
+ justify-content: flex-end;
1590
+ background: var(--scrim);
1591
+ }
1592
+ .pn__panel {
1593
+ position: relative;
1594
+ width: min(560px, 100%);
1595
+ height: 100%;
1596
+ box-sizing: border-box;
1597
+ display: flex;
1598
+ flex-direction: column;
1599
+ background: var(--bg-card);
1600
+ color: var(--ink);
1601
+ box-shadow: 0 0 0 1px var(--glass-edge), var(--shadow-pop);
1602
+ }
1603
+ .pn__head { position: relative; padding: 22px 64px 14px 24px; border-bottom: 1px solid var(--rule-soft); }
1604
+ .pn__eyebrow {
1605
+ margin: 0 0 6px;
1606
+ font-family: var(--label-face);
1607
+ font-size: 11px;
1608
+ letter-spacing: .12em;
1609
+ text-transform: uppercase;
1610
+ color: var(--craft, var(--ink-faint));
1611
+ }
1612
+ .pn__title { margin: 0; font-family: var(--font-display); font-weight: 400; font-size: 22px; letter-spacing: -0.02em; }
1613
+ .pn__close {
1614
+ position: absolute; top: 14px; right: 14px;
1615
+ display: inline-flex; align-items: center; justify-content: center;
1616
+ width: 40px; height: 40px; padding: 0; border: 0; border-radius: var(--radius-pill);
1617
+ background: none; color: var(--ink-soft); cursor: pointer;
1618
+ }
1619
+ .pn__close svg { width: 18px; height: 18px; }
1620
+ .pn__close:hover { color: var(--ink); background: var(--glass); }
1621
+ .pn__body { flex: 1; min-height: 0; overflow-y: auto; padding: 18px 24px 28px; display: flex; flex-direction: column; gap: 12px; }
1622
+ .pn__label { font-size: 13px; color: var(--ink-soft); }
1623
+ .pn__text {
1624
+ width: 100%;
1625
+ min-height: 260px;
1626
+ box-sizing: border-box;
1627
+ padding: 14px 16px;
1628
+ border: 1px solid var(--rule);
1629
+ border-radius: var(--radius-sm);
1630
+ background: var(--bg);
1631
+ color: var(--ink);
1632
+ font: inherit;
1633
+ font-size: 15px;
1634
+ line-height: 1.55;
1635
+ resize: vertical;
1636
+ }
1637
+ .pn__text:focus { outline: 2px solid var(--focus); outline-offset: 1px; }
1638
+ .pn__tools { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; }
1639
+ .pn__add {
1640
+ position: relative;
1641
+ display: inline-flex; align-items: center;
1642
+ min-height: 34px; padding: 0 14px;
1643
+ border-radius: var(--radius-pill);
1644
+ box-shadow: inset 0 0 0 1px var(--rule);
1645
+ font-size: 13.5px; color: var(--ink); cursor: pointer;
1646
+ }
1647
+ .pn__add:hover { background: var(--glass); }
1648
+ .pn__add:focus-within { outline: 2px solid var(--focus); outline-offset: 2px; }
1649
+ .pn__add input { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; }
1650
+ .pn__quiet { margin: 0; font-size: 13px; color: var(--ink-faint); }
1651
+ .pn__saved { margin-left: auto; }
1652
+ .pn__thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); gap: 8px; }
1653
+ .pn__thumb { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: 8px; background: var(--bg-deep); }
1654
+ .pn__error { margin: 0; font-size: 13.5px; color: var(--danger); }
1655
+ .pn__error:empty { display: none; }
1656
+ .pn__foot { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px; padding-top: 4px; }
1657
+ .pn__hint { margin: 0; font-size: 13px; line-height: 1.45; color: var(--ink-soft); }
1658
+ .pn__foot .pn__hint { flex: 1 1 240px; }
1659
+ .pn__submit {
1660
+ min-height: 40px; padding: 0 20px; border: 0; border-radius: var(--radius-pill);
1661
+ background: var(--accent); color: var(--accent-on); font: inherit; font-weight: 500; cursor: pointer;
1662
+ }
1663
+ .pn__submit:hover { background: var(--accent-lift); }
1664
+ .pn__submit:disabled { opacity: .55; cursor: default; }
1665
+ .pn__submit:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
1666
+ .pn__list { margin-top: 14px; padding-top: 16px; border-top: 1px solid var(--rule-soft); }
1667
+ .pn__list-h {
1668
+ margin: 0 0 8px;
1669
+ font-family: var(--label-face); font-size: var(--label-size); letter-spacing: var(--label-space);
1670
+ text-transform: var(--label-case); color: var(--ink-faint); font-weight: 400;
1671
+ }
1672
+ .pn__rows, .pn__ideas { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
1673
+ .pn__row {
1674
+ display: grid; gap: 2px; width: 100%; padding: 10px 12px; border: 0; border-radius: var(--radius-sm);
1675
+ background: none; color: var(--ink); font: inherit; text-align: left; cursor: pointer;
1676
+ }
1677
+ .pn__row:hover { background: var(--glass); }
1678
+ .pn__row:focus-visible, .pn__back:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
1679
+ .pn__row-meta { font-size: 12px; color: var(--ink-faint); }
1680
+ .pn__row-text { font-size: 14px; color: var(--ink-soft); }
1681
+ .pn__ideas a { display: inline-block; padding: 6px 0; color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
1682
+ .pn__back {
1683
+ align-self: flex-start; min-height: 32px; padding: 0 12px; margin-left: -12px; border: 0;
1684
+ border-radius: var(--radius-pill); background: none; color: var(--ink-soft); font: inherit; font-size: 13.5px; cursor: pointer;
1685
+ }
1686
+ .pn__back:hover { color: var(--ink); background: var(--glass); }
1687
+ .pn__done { margin: 0; font-size: 16px; }
1688
+ .pn-doc { font-size: 15px; line-height: 1.6; overflow-wrap: anywhere; }
1689
+ .pn-doc p, .pn-doc ul { margin: 0 0 12px; }
1690
+ .pn-doc__h { margin: 16px 0 6px; font-size: 16px; font-weight: 500; }
1691
+ .pn-doc__img { display: block; max-width: 100%; height: auto; margin: 8px 0; border-radius: 8px; }
1692
+ @media (max-width: 640px) {
1693
+ .pn__head { padding: 18px 60px 12px 16px; }
1694
+ .pn__body { padding: 14px 16px 24px; }
1695
+ }
1696
+
1553
1697
  /* THE BACK LINK (task 1004430): at the head of the bar when the page was
1554
1698
  reached from inside another hall page. 24px floor on both axes. */
1555
1699
  .topbar-back {
@@ -54,9 +54,9 @@
54
54
 
55
55
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
56
56
  time so the chrome exists before the deferred page scripts execute. -->
57
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
58
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
59
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
57
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
58
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
59
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
60
60
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
61
61
  <script src="/builders/palette.js" defer></script>
62
62
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
@@ -138,9 +138,9 @@
138
138
  </div>
139
139
  </div>
140
140
 
141
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
142
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
143
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
141
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
142
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
143
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
144
144
  <script src="/builders/palette.js" defer></script>
145
145
 
146
146
  <script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
@@ -142,9 +142,9 @@
142
142
 
143
143
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
144
144
 
145
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
146
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
147
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
145
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
146
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
147
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
148
148
  <script src="/builders/palette.js" defer></script>
149
149
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
150
150
  <script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
@@ -162,9 +162,9 @@
162
162
 
163
163
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
164
164
  chrome exists before the page script runs. -->
165
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
166
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
167
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
165
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
166
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
167
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
168
168
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
169
169
  <script src="/builders/palette.js" defer></script>
170
170
  <!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
@@ -171,9 +171,9 @@
171
171
 
172
172
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
173
173
  time so the chrome exists before the deferred page scripts execute. -->
174
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
175
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
176
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
174
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
175
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
176
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
177
177
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
178
178
  <script src="/builders/palette.js" defer></script>
179
179
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
@@ -0,0 +1,16 @@
1
+ # Page feedback docs — the Artistry palette (task 1004496, goal 1000095 WA6)
2
+
3
+ Covers: the palette tool in the top bar (`craft-mode.js` `TOOLS`), the feedback sheet (`page-notes.js`, `.pn*` and `.craft-tool*` in `style.css`). The store and its rules are `modules/page-notes/CLAUDE.md`.
4
+
5
+ ## What it is
6
+
7
+ - In **Artistry** mode a round palette button sits right before the mode switch ("Feedback" beside it from 1181px up, the glyph alone below, as the switch folds). Pressing it opens a sheet from the right: "What should this page be?", a long text area, **Add images** (PNG, JPG, GIF, WebP, 4 MB each, eight per doc), and **Turn into ideas**. The draft saves as you type; closing loses nothing, and reopening on the same page resumes it.
8
+ - On submit the server's AI split files each point as its own idea under the artist's name; the sheet shows the ideas it became. Under the writing area, **This page's reference docs** lists the page's submitted docs; each opens in place.
9
+ - **`?note=N` on any hall page opens that doc on arrival, in any mode.** Every idea a doc became links there, which is how the doc is retrievable from its ideas.
10
+
11
+ ## The rules an edit here must keep
12
+
13
+ - **The tool's code is not on every page.** `craft-mode.js` loads `page-notes.js` the first time the palette is pressed (or a `?note=` link arrives), at the same `?v` as itself. A new mode tool (paintbrush 1004497, lightbulb 1004498) is a `TOOLS` entry with its own script, not more code in `craft-mode.js`.
14
+ - **The reader shows only this module's own images** (`/api/bongos/page-notes/images/<id>`); any other image markdown stays text, and the doc is escaped. `tests/page_notes_panel.mjs` pins it.
15
+ - **The page id is the page inventory's** — `builders:` plus `body[data-page]` — so a doc keys to the same id every other page record uses.
16
+ - **Colours stay in `style.css`.** The tool takes `--craft` (falling back to ink outside a looked mode); the submit button keeps the accent, because it is the thing to press.
@@ -42,6 +42,8 @@ function blockerDocketMatter(b, mayResolve) {
42
42
  tally: null,
43
43
  href: `/blockers#blocker-${b.id}`,
44
44
  ref: { type: 'blocker', id: b.id },
45
+ // the card's "more" (task 1004566): the body GET /blockers shows everyone
46
+ detail: b.body_md || null,
45
47
  // the floor desk's one act (task 1004560): decide, by one of its options
46
48
  ...(mayResolve ? { acts: [{ key: 'decide', options: blockerOptions(b.body_md) }] } : {}),
47
49
  };
@@ -41,6 +41,8 @@ function overrideMatter(r, mayDecide) {
41
41
  tally: null,
42
42
  href: `/task/${encodeURIComponent(r.task_id)}`,
43
43
  ref: { type: 'override_request', id: r.id },
44
+ // the card's "more" (task 1004566): the reason GET /override-requests shows
45
+ detail: r.rationale || null,
44
46
  };
45
47
  }
46
48
 
@@ -58,6 +60,8 @@ function removalMatter(r) {
58
60
  tally: null,
59
61
  href: `/task/${encodeURIComponent(r.proposal_task_id)}`,
60
62
  ref: { type: 'task', id: r.proposal_task_id },
63
+ // the card's "more" (task 1004566): the proposal's own text, from the same read
64
+ detail: r.description || null,
61
65
  // POST /removal-proposals/:id/decide, the docket source's own atom (task 1004559)
62
66
  acts: [{ key: 'remove' }, { key: 'keep' }],
63
67
  };
@@ -236,6 +236,8 @@ function accessRequestDocketMatter(r) {
236
236
  tally: null,
237
237
  href: '/watch',
238
238
  ref: { type: 'access_request', id: r.id },
239
+ // the card's "more" (task 1004566): their note, as GET /access-requests shows it
240
+ detail: r.note || null,
239
241
  };
240
242
  }
241
243
 
@@ -0,0 +1,15 @@
1
+ # @bongos/core — installed platform dependency (not your project's CLAUDE.md)
2
+
3
+ This file is a placeholder shipped inside the **@bongos/core** package. The core's own
4
+ internal agent notes are intentionally omitted from the published artifact so they cannot
5
+ load into your instance's context.
6
+
7
+ **It does not govern this repository.** Your own root `CLAUDE.md`, your `.claude/`
8
+ directory, and the live Bongos database are the source of truth for how work happens here
9
+ and who may do what.
10
+
11
+ You are seeing this only because an agent opened a file under `node_modules/@bongos/core/`.
12
+ Nothing in here needs to be read to use the platform — run `bongos help` for commands.
13
+
14
+ _To work ON the platform core itself, use the standalone @bongos/core source repository,
15
+ not this vendored copy._
@@ -0,0 +1,147 @@
1
+ // modules/page-notes/db.js — the thin SQL behind a page feedback doc (task
2
+ // 1004496). Every function takes an optional executor so a test can hand in a
3
+ // fake query function; the default is the shared pool.
4
+
5
+ 'use strict';
6
+
7
+ const { pool } = require('../../src/module-api');
8
+
9
+ const exec = (executor) => executor || ((text, params) => pool.query(text, params));
10
+
11
+ const COLUMNS = `n.id, n.page_id, n.page_path, n.page_title, n.body_md, n.author_id, n.status, n.split_by,
12
+ n.idea_ids, n.created_at, n.updated_at, n.submitted_at, n.filed_at,
13
+ b.github_login AS author_login, COALESCE(NULLIF(b.display_name, ''), b.github_login) AS author_name`;
14
+
15
+ // The builder's open draft for a page, made if there is none. ON CONFLICT on
16
+ // the one-draft index makes two tabs opening at once resolve to one row.
17
+ async function openDraft({ authorId, pageId, pagePath, pageTitle }, executor) {
18
+ const q = exec(executor);
19
+ await q(
20
+ `INSERT INTO page_notes_docs (page_id, page_path, page_title, author_id)
21
+ VALUES ($1, $2, $3, $4)
22
+ ON CONFLICT (author_id, page_id) WHERE status = 'draft' DO NOTHING`,
23
+ [pageId, pagePath, pageTitle, authorId],
24
+ );
25
+ const r = await q(
26
+ `SELECT ${COLUMNS} FROM page_notes_docs n JOIN builders b ON b.id = n.author_id
27
+ WHERE n.author_id = $1 AND n.page_id = $2 AND n.status = 'draft'`,
28
+ [authorId, pageId],
29
+ );
30
+ return r.rows[0] || null;
31
+ }
32
+
33
+ async function getNote(id, executor) {
34
+ const q = exec(executor);
35
+ const r = await q(`SELECT ${COLUMNS} FROM page_notes_docs n JOIN builders b ON b.id = n.author_id WHERE n.id = $1`, [id]);
36
+ return r.rows[0] || null;
37
+ }
38
+
39
+ // A page's submitted docs, newest first, with no body (the list shows an excerpt).
40
+ async function listForPage(pageId, { limit = 20 } = {}, executor) {
41
+ const q = exec(executor);
42
+ const r = await q(
43
+ `SELECT n.id, n.page_id, n.page_title, n.status, n.submitted_at, n.idea_ids,
44
+ left(n.body_md, 400) AS excerpt,
45
+ COALESCE(NULLIF(b.display_name, ''), b.github_login) AS author_name
46
+ FROM page_notes_docs n JOIN builders b ON b.id = n.author_id
47
+ WHERE n.page_id = $1 AND n.status <> 'draft'
48
+ ORDER BY n.submitted_at DESC NULLS LAST, n.id DESC
49
+ LIMIT $2`,
50
+ [pageId, limit],
51
+ );
52
+ return r.rows;
53
+ }
54
+
55
+ async function saveBody(id, authorId, bodyMd, executor) {
56
+ const q = exec(executor);
57
+ const r = await q(
58
+ `UPDATE page_notes_docs SET body_md = $3, updated_at = now()
59
+ WHERE id = $1 AND author_id = $2 AND status = 'draft'
60
+ RETURNING id, updated_at`,
61
+ [id, authorId, bodyMd],
62
+ );
63
+ return r.rows[0] || null;
64
+ }
65
+
66
+ async function countImages(noteId, executor) {
67
+ const q = exec(executor);
68
+ const r = await q('SELECT count(*)::int AS n FROM page_notes_images WHERE note_id = $1', [noteId]);
69
+ return r.rows[0] ? r.rows[0].n : 0;
70
+ }
71
+
72
+ async function addImage(noteId, { content_type: type, bytes, name }, executor) {
73
+ const q = exec(executor);
74
+ const r = await q(
75
+ `INSERT INTO page_notes_images (note_id, content_type, name, bytes)
76
+ VALUES ($1, $2, $3, $4) RETURNING id, name, content_type, octet_length(bytes) AS size`,
77
+ [noteId, type, name, bytes],
78
+ );
79
+ return r.rows[0] || null;
80
+ }
81
+
82
+ async function listImages(noteId, executor) {
83
+ const q = exec(executor);
84
+ const r = await q(
85
+ 'SELECT id, name, content_type, octet_length(bytes) AS size FROM page_notes_images WHERE note_id = $1 ORDER BY id',
86
+ [noteId],
87
+ );
88
+ return r.rows;
89
+ }
90
+
91
+ // One image with the status and author of the doc it belongs to, which is what
92
+ // decides who may see it.
93
+ async function getImage(id, executor) {
94
+ const q = exec(executor);
95
+ const r = await q(
96
+ `SELECT i.id, i.content_type, i.bytes, n.status, n.author_id
97
+ FROM page_notes_images i JOIN page_notes_docs n ON n.id = i.note_id WHERE i.id = $1`,
98
+ [id],
99
+ );
100
+ return r.rows[0] || null;
101
+ }
102
+
103
+ // Submit: the draft becomes 'splitting', and images its text no longer shows
104
+ // are dropped. Conditional on 'draft', so a double click submits once.
105
+ async function markSplitting(id, authorId, keepImageIds, executor) {
106
+ const q = exec(executor);
107
+ const r = await q(
108
+ `UPDATE page_notes_docs SET status = 'splitting', submitted_at = now(), updated_at = now()
109
+ WHERE id = $1 AND author_id = $2 AND status = 'draft' RETURNING id`,
110
+ [id, authorId],
111
+ );
112
+ if (!r.rows[0]) return false;
113
+ await q('DELETE FROM page_notes_images WHERE note_id = $1 AND NOT (id = ANY($2::bigint[]))', [id, keepImageIds]);
114
+ return true;
115
+ }
116
+
117
+ async function markFiled(id, { ideaIds, splitBy }, executor) {
118
+ const q = exec(executor);
119
+ const r = await q(
120
+ `UPDATE page_notes_docs SET status = 'filed', idea_ids = $2::bigint[], split_by = $3, filed_at = now(), updated_at = now()
121
+ WHERE id = $1 AND status = 'splitting' RETURNING id`,
122
+ [id, ideaIds, splitBy],
123
+ );
124
+ return !!r.rows[0];
125
+ }
126
+
127
+ // The ideas a doc became, by title, for the doc's own view.
128
+ async function ideaTitles(ids, executor) {
129
+ if (!ids || !ids.length) return [];
130
+ const q = exec(executor);
131
+ const r = await q('SELECT id, title, status FROM idea_inbox WHERE id = ANY($1::bigint[]) ORDER BY id', [ids]);
132
+ return r.rows;
133
+ }
134
+
135
+ module.exports = {
136
+ openDraft,
137
+ getNote,
138
+ listForPage,
139
+ saveBody,
140
+ countImages,
141
+ addImage,
142
+ listImages,
143
+ getImage,
144
+ markSplitting,
145
+ markFiled,
146
+ ideaTitles,
147
+ };
@@ -0,0 +1,68 @@
1
+ -- page_notes_001_notes.sql — an artist's feedback doc on one hall page, and the
2
+ -- reference images it carries (task 1004496, goal 1000095 WA6).
3
+ --
4
+ -- In Artistry mode the palette button opens a feedback doc for the page you are
5
+ -- on. The doc can be as long as it needs to be and carries uploaded reference
6
+ -- images. On submit an AI splits it into separate ideas in the idea inbox, each
7
+ -- attributed to the artist, and the doc itself STAYS as the page's design
8
+ -- reference: later tasks on that page can cite it, and every idea it became
9
+ -- links back to it.
10
+ --
11
+ -- WHY A ROW AND NOT THE COPY DESK. The copy desk's rule is "no live CMS": it may
12
+ -- hold no wording that could become product copy. A feedback doc is not product
13
+ -- copy — it is a person's judgement about a page, like an idea's body — so it
14
+ -- lives in its own module, and nothing here is ever rendered as the page's text.
15
+ --
16
+ -- page_id is the page inventory's stable id, `<surface>:<page>` (docs/page-
17
+ -- inventory.json). page_path is where the doc's links lead back to.
18
+ -- Images live in the database, not on disk: a doc holds a handful of
19
+ -- screenshots, and a row survives every core upgrade and backup with no path
20
+ -- to get wrong (the task-visuals store had to be moved once for exactly that).
21
+
22
+ CREATE TABLE IF NOT EXISTS page_notes_docs (
23
+ id BIGSERIAL PRIMARY KEY,
24
+ page_id TEXT NOT NULL,
25
+ page_path TEXT NOT NULL DEFAULT '',
26
+ page_title TEXT NOT NULL DEFAULT '',
27
+ body_md TEXT NOT NULL DEFAULT '',
28
+ author_id BIGINT NOT NULL REFERENCES builders(id),
29
+ status TEXT NOT NULL DEFAULT 'draft',
30
+ split_by TEXT,
31
+ idea_ids BIGINT[] NOT NULL DEFAULT '{}',
32
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
33
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
34
+ submitted_at TIMESTAMPTZ,
35
+ filed_at TIMESTAMPTZ,
36
+ CONSTRAINT page_notes_page_id_chk CHECK (page_id ~ '^[a-z][a-z0-9-]{0,31}:[a-z0-9][a-z0-9_-]{0,63}$'),
37
+ CONSTRAINT page_notes_page_path_chk CHECK (length(page_path) <= 200 AND (page_path = '' OR page_path ~ '^/[A-Za-z0-9/_.-]*$')),
38
+ CONSTRAINT page_notes_page_title_chk CHECK (length(page_title) <= 120),
39
+ CONSTRAINT page_notes_body_chk CHECK (length(body_md) <= 40000),
40
+ CONSTRAINT page_notes_status_chk CHECK (status IN ('draft', 'splitting', 'filed')),
41
+ CONSTRAINT page_notes_split_by_chk CHECK (split_by IS NULL OR split_by IN ('ai', 'whole')),
42
+ CONSTRAINT page_notes_ideas_bounded CHECK (cardinality(idea_ids) <= 20)
43
+ );
44
+
45
+ -- One open draft per builder per page: reopening the palette resumes it.
46
+ CREATE UNIQUE INDEX IF NOT EXISTS page_notes_one_draft
47
+ ON page_notes_docs (author_id, page_id) WHERE status = 'draft';
48
+ -- The page's reference docs, newest first.
49
+ CREATE INDEX IF NOT EXISTS page_notes_by_page
50
+ ON page_notes_docs (page_id, submitted_at DESC) WHERE status <> 'draft';
51
+
52
+ CREATE TABLE IF NOT EXISTS page_notes_images (
53
+ id BIGSERIAL PRIMARY KEY,
54
+ note_id BIGINT NOT NULL REFERENCES page_notes_docs(id) ON DELETE CASCADE,
55
+ content_type TEXT NOT NULL,
56
+ name TEXT NOT NULL DEFAULT '',
57
+ bytes BYTEA NOT NULL,
58
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
59
+ -- SVG is deliberately absent: it is an executable document, and these are
60
+ -- served from our own origin (the task-visuals rule).
61
+ CONSTRAINT page_notes_images_type_chk CHECK (content_type IN ('image/png', 'image/jpeg', 'image/gif', 'image/webp')),
62
+ CONSTRAINT page_notes_images_size_chk CHECK (octet_length(bytes) BETWEEN 1 AND 4194304),
63
+ CONSTRAINT page_notes_images_name_chk CHECK (length(name) <= 120)
64
+ );
65
+ CREATE INDEX IF NOT EXISTS page_notes_images_by_note ON page_notes_images (note_id);
66
+
67
+ INSERT INTO schema_migrations (version) VALUES ('page_notes_001_notes')
68
+ ON CONFLICT DO NOTHING;