@bongos/core 1.19.1081 → 1.20.1

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 (251) hide show
  1. package/.bongos-core.json +526 -191
  2. package/.claude/skills/planning-session/SKILL.md +6 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +32 -0
  5. package/clients/bongos-client/index.cjs +32 -0
  6. package/clients/bongos-client/index.d.ts +51 -6
  7. package/clients/bongos-client/index.mjs +32 -0
  8. package/docs/adr/0099-delayed-redacted-mirror-export.md +5 -1
  9. package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
  10. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +17 -0
  11. package/docs/adr/0176-private-repo-deploy-keys.md +2 -0
  12. package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
  13. package/docs/adr/0348-the-web-tier-may-look-read-only-at-what-an-owners-render-key-can-see.md +53 -0
  14. package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +106 -0
  15. package/docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md +103 -0
  16. package/docs/adr/README.md +3 -0
  17. package/docs/api/openapi.json +851 -19
  18. package/docs/api-reference.md +33 -9
  19. package/docs/architecture.md +6 -2
  20. package/docs/copy-inventory.md +620 -553
  21. package/docs/copy-registry.json +1467 -820
  22. package/docs/file-map.md +2 -0
  23. package/docs/module-api-changelog.md +6 -0
  24. package/docs/modules-contract.md +1 -0
  25. package/docs/page-inventory.json +38 -4
  26. package/docs/page-readings.json +1674 -1566
  27. package/migrations/core_260_goals_working_area.sql +63 -0
  28. package/migrations/core_261_grade_attempts_verdicts.sql +24 -0
  29. package/migrations/core_262_drop_builder_box_blocked.sql +33 -0
  30. package/modules/agents/lib/validate.js +43 -0
  31. package/modules/autonomy/gauge.js +38 -2
  32. package/modules/grading/grader-subagent.js +37 -2
  33. package/modules/grading/grader-workers/reader.js +65 -5
  34. package/modules/hall-ui/public/approval-queue.css +33 -7
  35. package/modules/hall-ui/public/approval-queue.js +9 -2
  36. package/modules/hall-ui/public/atlas.html +1 -1
  37. package/modules/hall-ui/public/blockers.html +1 -1
  38. package/modules/hall-ui/public/board-room.html +1 -1
  39. package/modules/hall-ui/public/brand-holes.js +121 -0
  40. package/modules/hall-ui/public/collab.html +1 -1
  41. package/modules/hall-ui/public/collab.js +1 -1
  42. package/modules/hall-ui/public/copy-desk.html +1 -1
  43. package/modules/hall-ui/public/deploy.html +6 -1
  44. package/modules/hall-ui/public/deploy.js +22 -8
  45. package/modules/hall-ui/public/diagrams.html +1 -1
  46. package/modules/hall-ui/public/dom-utils.js +20 -0
  47. package/modules/hall-ui/public/drachmae.html +1 -1
  48. package/modules/hall-ui/public/fleet.html +1 -1
  49. package/modules/hall-ui/public/gate.html +1 -1
  50. package/modules/hall-ui/public/goals.html +1 -1
  51. package/modules/hall-ui/public/government.html +1 -1
  52. package/modules/hall-ui/public/idea.html +1 -1
  53. package/modules/hall-ui/public/idea.js +28 -0
  54. package/modules/hall-ui/public/ideas.html +1 -1
  55. package/modules/hall-ui/public/ideas.js +19 -3
  56. package/modules/hall-ui/public/index.html +5 -1
  57. package/modules/hall-ui/public/modules.html +1 -1
  58. package/modules/hall-ui/public/primer.html +1 -1
  59. package/modules/hall-ui/public/profile-nudge.js +21 -4
  60. package/modules/hall-ui/public/profile.html +1 -1
  61. package/modules/hall-ui/public/profile.js +1 -1
  62. package/modules/hall-ui/public/project-settings.html +1 -1
  63. package/modules/hall-ui/public/ranks.html +1 -1
  64. package/modules/hall-ui/public/roadmap.html +1 -1
  65. package/modules/hall-ui/public/roster.html +1 -1
  66. package/modules/hall-ui/public/sessions.html +1 -1
  67. package/modules/hall-ui/public/settings.html +19 -30
  68. package/modules/hall-ui/public/settings.js +24 -177
  69. package/modules/hall-ui/public/settings.states.json +1 -1
  70. package/modules/hall-ui/public/shell.js +4 -2
  71. package/modules/hall-ui/public/studio.css +14 -8
  72. package/modules/hall-ui/public/studio.html +5 -4
  73. package/modules/hall-ui/public/task.html +1 -1
  74. package/modules/hall-ui/public/thinking.css +6 -3
  75. package/modules/hall-ui/public/thinking.html +1 -1
  76. package/modules/hall-ui/public/tweak-editor-lib.js +82 -1
  77. package/modules/hall-ui/public/tweak-editor.css +142 -11
  78. package/modules/hall-ui/public/tweak-editor.html +25 -9
  79. package/modules/hall-ui/public/tweak-editor.js +243 -10
  80. package/modules/hall-ui/public/watch.html +1 -1
  81. package/modules/hall-ui/public/work.html +1 -1
  82. package/modules/hall-ui/records/approval-queue.md +6 -0
  83. package/modules/hall-ui/records/tweak-editor.md +6 -0
  84. package/modules/ideas/ratify.js +36 -8
  85. package/modules/ideas/routes/ratify-goal.js +6 -0
  86. package/modules/lifecycle/db-analytics.js +79 -10
  87. package/modules/lifecycle/db-claims.js +48 -1
  88. package/modules/lifecycle/db-deps-criteria.js +94 -40
  89. package/modules/lifecycle/db-goals.js +4 -1
  90. package/modules/lifecycle/db-grade.js +9 -1
  91. package/modules/lifecycle/db-relevance-flags.js +183 -0
  92. package/modules/lifecycle/db-versions.js +4 -3
  93. package/modules/lifecycle/db.js +19 -0
  94. package/modules/lifecycle/done-when.js +5 -3
  95. package/modules/lifecycle/est-advisory.js +31 -4
  96. package/modules/lifecycle/goal-task-relevance-advisory.js +153 -0
  97. package/modules/lifecycle/goal-task-relevance-judge.js +226 -0
  98. package/modules/lifecycle/migrations/lifecycle_014_goal_task_relevance_flags.sql +82 -0
  99. package/modules/lifecycle/module.json +2 -1
  100. package/modules/lifecycle/routes/claims.js +8 -1
  101. package/modules/lifecycle/routes/goal-task-relevance.js +72 -0
  102. package/modules/lifecycle/routes/goals.js +1 -1
  103. package/modules/lifecycle/routes/tasks.js +27 -24
  104. package/modules/lifecycle/routes/visuals.js +9 -0
  105. package/modules/lifecycle/ship-preflight.js +7 -11
  106. package/modules/lifecycle/task-visuals.js +109 -5
  107. package/modules/npm-release/module.json +2 -1
  108. package/modules/npm-release/preview/commands.js +81 -0
  109. package/modules/npm-release/preview/divert.js +95 -0
  110. package/modules/npm-release/preview/env.js +78 -0
  111. package/modules/npm-release/preview/proxy.js +107 -0
  112. package/modules/npm-release/preview/runtime.js +83 -0
  113. package/modules/npm-release/preview/supervisor.js +175 -0
  114. package/modules/npm-release/public/work.js +65 -1
  115. package/modules/npm-release/routes/preview.js +133 -0
  116. package/modules/provisioning/app-status.js +83 -0
  117. package/modules/provisioning/module.json +2 -1
  118. package/modules/provisioning/pollers/app-liveness.js +110 -0
  119. package/modules/provisioning/render-lookup.js +68 -0
  120. package/modules/provisioning/render-standup.js +44 -1
  121. package/modules/provisioning/routes/render-standup.js +81 -6
  122. package/modules/provisioning/starter-bundles.js +4 -1
  123. package/modules/public-landing/public/account-private.states.json +14 -0
  124. package/modules/public-landing/public/account.html +513 -0
  125. package/modules/public-landing/public/account.probes.json +37 -0
  126. package/modules/public-landing/public/account.states.json +16 -0
  127. package/modules/public-landing/public/index.html +5 -2
  128. package/modules/public-landing/public/projects.html +661 -18
  129. package/modules/public-landing/public/projects.states.json +8 -1
  130. package/modules/render-deploy/deploys.js +203 -0
  131. package/modules/render-deploy/migrations/render_deploy_001_app.sql +22 -0
  132. package/modules/render-deploy/module.json +25 -0
  133. package/modules/render-deploy/public/deploy.css +10 -0
  134. package/modules/render-deploy/public/deploy.js +249 -0
  135. package/modules/render-deploy/render.js +56 -0
  136. package/modules/render-deploy/routes/act.js +88 -0
  137. package/modules/render-deploy/routes/app.js +65 -0
  138. package/modules/render-deploy/routes/history.js +38 -0
  139. package/modules/specialities/db.js +27 -8
  140. package/modules/specialities/migrations/specialities_003_skills.sql +46 -0
  141. package/modules/specialities/routes/specialities.js +51 -3
  142. package/modules/specialities/skills.js +94 -0
  143. package/modules/specialities/specialities.js +56 -9
  144. package/modules/ui-design/kit/fixtures/me-cross-project-private.json +12 -0
  145. package/modules/ui-design/kit/fixtures/me__cross-project.json +15 -0
  146. package/modules/ui-design/kit/fixtures/provisioning-instance-render.json +39 -0
  147. package/modules/ui-design/kit/lib.js +3 -1
  148. package/modules/ui-design/kit/serve.js +96 -0
  149. package/package-lock.json +2 -2
  150. package/package.json +1 -1
  151. package/release-notes.json +163 -0
  152. package/scripts/gds/agents-sync.js +13 -3
  153. package/scripts/gds/autobongos-run.js +80 -2
  154. package/scripts/gds/autobongos-service.cmd +12 -0
  155. package/scripts/gds/copy-apply.js +14 -0
  156. package/scripts/gds/dev-box-guard.js +3 -2
  157. package/scripts/gds/fitness.js +9 -0
  158. package/scripts/gds/grade-correlation-audit.js +42 -12
  159. package/scripts/gds/grade-replay.js +1 -1
  160. package/scripts/gds/provision-core-upgrade.js +38 -7
  161. package/scripts/gds/provision-pin-key.js +176 -0
  162. package/scripts/gds/provision-render.js +62 -31
  163. package/scripts/gds/provision-teardown.js +7 -2
  164. package/scripts/gds/provision-units.js +31 -0
  165. package/scripts/gds/provision.js +10 -10
  166. package/scripts/gds/publish-manifest.js +1 -0
  167. package/scripts/gds/render-api.js +19 -0
  168. package/scripts/gds/render-payload.js +39 -65
  169. package/scripts/gds/run-unit-tests.js +10 -0
  170. package/scripts/gds/ship-finish.js +19 -20
  171. package/scripts/gds/smoke-dependencies.sh +37 -8
  172. package/scripts/gds/status.js +12 -4
  173. package/scripts/gds/upgrade.js +2 -2
  174. package/scripts/public-mirror-export.js +7 -1
  175. package/src/bongos/module-scope-map.js +13 -0
  176. package/src/bongos/serve-internal.js +3 -1
  177. package/src/module-api.js +10 -1
  178. package/src/platform-server.js +39 -0
  179. package/tests/account_privacy_flags.mjs +10 -6
  180. package/tests/agents_authoring.mjs +15 -1
  181. package/tests/agents_sync.mjs +106 -5
  182. package/tests/agents_validate.mjs +45 -0
  183. package/tests/api_path_404.mjs +6 -0
  184. package/tests/autobongos_cadence.mjs +7 -0
  185. package/tests/autobongos_loop.mjs +136 -1
  186. package/tests/autonomy_gauge.mjs +62 -0
  187. package/tests/blocker_hall_live_proof.mjs +275 -0
  188. package/tests/claim_gate_ci_unblock.mjs +116 -0
  189. package/tests/claim_gate_rebase.mjs +15 -3
  190. package/tests/collab_page.mjs +10 -0
  191. package/tests/conductor_main_e2e.mjs +97 -0
  192. package/tests/core_262_drop_box_blocked_db.mjs +143 -0
  193. package/tests/core_upgrade_runner.mjs +1 -0
  194. package/tests/dependency_writes_atomic.mjs +191 -0
  195. package/tests/deploy_page_projects.mjs +2 -1
  196. package/tests/effective_visibility_predicate.mjs +15 -0
  197. package/tests/est_advisory.mjs +27 -0
  198. package/tests/goal_map_page.mjs +26 -0
  199. package/tests/goal_task_relevance.mjs +518 -0
  200. package/tests/goal_working_area.mjs +146 -0
  201. package/tests/grade_attempts.mjs +61 -0
  202. package/tests/grade_attribution.mjs +23 -0
  203. package/tests/grade_correlation_audit.mjs +54 -1
  204. package/tests/grader_reader_lens.mjs +55 -2
  205. package/tests/grader_root_outage.mjs +6 -1
  206. package/tests/grader_subagent_tools_arg.mjs +102 -0
  207. package/tests/hall_approval_queue.mjs +54 -2
  208. package/tests/hall_settings_world.mjs +3 -1
  209. package/tests/hall_tweak_editor.mjs +316 -11
  210. package/tests/hub_account_page.mjs +394 -0
  211. package/tests/idea_detail_page.mjs +64 -0
  212. package/tests/idea_goal_ratification.mjs +64 -0
  213. package/tests/idea_spark_hall.mjs +60 -9
  214. package/tests/ideator_full_idea_shapes_space_proof.mjs +1 -1
  215. package/tests/landing_page.mjs +6 -3
  216. package/tests/module-scope-map.mjs +31 -8
  217. package/tests/module_api.mjs +1 -0
  218. package/tests/module_loader.mjs +1 -1
  219. package/tests/nav_permission_atoms.mjs +1 -1
  220. package/tests/no_phantom_mirror_workflow.mjs +24 -0
  221. package/tests/npm_release_preview_commands.mjs +89 -0
  222. package/tests/npm_release_preview_divert.mjs +164 -0
  223. package/tests/npm_release_preview_env.mjs +100 -0
  224. package/tests/npm_release_preview_proxy.mjs +162 -0
  225. package/tests/npm_release_preview_routes.mjs +208 -0
  226. package/tests/npm_release_preview_supervisor.mjs +203 -0
  227. package/tests/pin_write_key.mjs +383 -0
  228. package/tests/planning_session_skill.mjs +27 -0
  229. package/tests/platform_boot.mjs +71 -0
  230. package/tests/profile_nudge_links.mjs +86 -0
  231. package/tests/profile_ui_cross_project.mjs +1 -1
  232. package/tests/projects_hub.mjs +2 -2
  233. package/tests/projects_hub_app_status.mjs +249 -0
  234. package/tests/projects_hub_app_step.mjs +282 -52
  235. package/tests/projects_hub_render_connect.mjs +295 -0
  236. package/tests/provision_render.mjs +121 -28
  237. package/tests/provision_settings_apply.mjs +62 -0
  238. package/tests/provisioning_app_status.mjs +204 -0
  239. package/tests/provisioning_render_route.mjs +126 -6
  240. package/tests/render_check.mjs +2 -1
  241. package/tests/render_deploy.mjs +495 -0
  242. package/tests/ship_preflight.mjs +61 -8
  243. package/tests/smoke_dependencies_witness.mjs +46 -0
  244. package/tests/speciality_skills.mjs +214 -0
  245. package/tests/studio_room_height.mjs +70 -0
  246. package/tests/task_detail_includes.mjs +10 -0
  247. package/tests/task_visuals_instance_root.mjs +150 -0
  248. package/tests/tweak_batch_apply.mjs +21 -0
  249. package/tests/ui_design_kit.mjs +36 -4
  250. package/tests/upgrade.mjs +1 -1
  251. package/tests/upgrade_persist_pin.mjs +24 -0
@@ -26,6 +26,7 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
26
26
  const PUB = path.join(ROOT, 'modules/hall-ui/public');
27
27
  const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8');
28
28
  const lib = require('../modules/hall-ui/public/tweak-editor-lib.js');
29
+ const holes = require('../modules/hall-ui/public/brand-holes.js');
29
30
  const pages = require('../modules/copy-desk/pages.js');
30
31
  const pageStatus = require('../modules/copy-desk/page-status.js');
31
32
 
@@ -69,7 +70,7 @@ const NEXT = {
69
70
  // ── the mini-DOM over the page's own markup ────────────────────────────────
70
71
  const HTML = read('tweak-editor.html');
71
72
  const BODY = /<body[^>]*>([\s\S]*)<\/body>/.exec(HTML)[1].replace(/<script[\s\S]*?<\/script>/g, '');
72
- const SOURCES = ['dom-utils.js', 'tweak-editor-lib.js', 'tweak-editor.js'].map((f) => [f, read(f)]);
73
+ const SOURCES = ['dom-utils.js', 'brand-holes.js', 'tweak-editor-lib.js', 'tweak-editor.js'].map((f) => [f, read(f)]);
73
74
 
74
75
  function augment(el) {
75
76
  if (el._aug) return el;
@@ -95,12 +96,13 @@ function augment(el) {
95
96
  el.scrollHeight = 40;
96
97
  el.inert = false;
97
98
  el.focus = () => { boot.focused = el; };
99
+ el.scrollIntoView = () => {};
98
100
  el.click = () => { el.clicked = (el.clicked || 0) + 1; };
99
101
  return el;
100
102
  }
101
103
  let boot = null;
102
104
 
103
- function bootEditor({ search = '?page=landing:index', answers = {} } = {}) {
105
+ function bootEditor({ search = '?page=landing:index', answers = {}, stored = {} } = {}) {
104
106
  const root = augment(makeEl('body', {}));
105
107
  root.innerHTML = BODY;
106
108
  const byId = (id) => walk(root, (c) => (c.id === id ? c : null));
@@ -109,6 +111,7 @@ function bootEditor({ search = '?page=landing:index', answers = {} } = {}) {
109
111
  let timerId = 0;
110
112
  const requests = [];
111
113
  const fetches = [];
114
+ const store = { ...stored };
112
115
  const documentObj = {
113
116
  title: 'Tweak a page — Cloud Bongos',
114
117
  body: root,
@@ -130,30 +133,35 @@ function bootEditor({ search = '?page=landing:index', answers = {} } = {}) {
130
133
  },
131
134
  };
132
135
  const windowObj = {
133
- __BRANDING__: {}, __MODULES__: { enabled: {} },
136
+ __BRANDING__: { identity: { productName: 'Cloud Bongos' } }, __MODULES__: { enabled: {} },
134
137
  BongosClient: { createClient: () => client },
135
- location: { host: 'builders.cloudbongos.com', hostname: 'builders.cloudbongos.com', protocol: 'https:', search, pathname: '/builders/tweak-editor', assign: (u) => { out.navigated = u; } },
138
+ location: { host: 'builders.cloudbongos.com', hostname: 'builders.cloudbongos.com', protocol: 'https:', origin: 'https://builders.cloudbongos.com', href: 'https://builders.cloudbongos.com/builders/tweak-editor', search, pathname: '/builders/tweak-editor', assign: (u) => { out.navigated = u; } },
136
139
  history: { replaceState: (_, __, u) => { out.replaced = u; } },
137
- localStorage: { getItem: () => 'off', setItem() {} },
140
+ localStorage: { getItem: (k) => (String(k).startsWith('hall-tweak') ? (store[k] === undefined ? null : store[k]) : 'off'), setItem: (k, v) => { store[k] = v; } },
138
141
  addEventListener() {},
142
+ innerHeight: 1000,
143
+ screen: { availWidth: 1600, availHeight: 1000, availLeft: 0, availTop: 0 },
144
+ open: (...a) => { out.opened.push(a); return {}; },
139
145
  setTimeout: (fn, ms) => { const id = ++timerId; timers.push({ id, fn, ms }); return id; },
140
146
  clearTimeout: (id) => { const i = timers.findIndex((t) => t.id === id); if (i >= 0) timers.splice(i, 1); },
141
147
  };
142
148
  const sandbox = {
143
149
  window: windowObj, document: documentObj, location: windowObj.location,
144
150
  fetch: async (url, init) => { fetches.push({ url, init }); const a = answers[`FETCH ${init.method} ${url.replace(/^\/api\/bongos/, '')}`]; return { ok: !!(a && a.ok), json: async () => (a ? a.data : null) }; },
145
- console, URLSearchParams, setTimeout, clearTimeout,
151
+ console, URL, URLSearchParams, setTimeout, clearTimeout,
146
152
  };
147
153
  sandbox.globalThis = sandbox;
148
154
  vm.createContext(sandbox);
149
155
  for (const [name, src] of SOURCES) vm.runInContext(src, sandbox, { filename: name });
150
156
 
151
- const fire = (type, target) => (docListeners[type] || []).forEach((fn) => fn({ type, target }));
157
+ const fire = (type, target, extra) => (docListeners[type] || []).forEach((fn) => fn({ type, target, ...(extra || {}) }));
152
158
  const out = {
153
- requests, fetches, navigated: null, replaced: null, focused: null,
159
+ requests, fetches, store, opened: [], navigated: null, replaced: null, focused: null,
154
160
  el: byId,
155
161
  window: windowObj,
156
162
  lines: () => root.querySelectorAll('textarea'),
163
+ fire: (type, target, extra) => fire(type, target, extra),
164
+ key: (id, keyName, shiftKey) => { const el = byId(id); const ev = { key: keyName, shiftKey: !!shiftKey, prevented: false, preventDefault() { ev.prevented = true; } }; fire('keydown', el, ev); return ev; },
157
165
  type: (key, value) => { const ta = byId(`tw-${key}`); ta.value = value; fire('input', ta); },
158
166
  click: async (id) => { const el = byId(id); assert.ok(el, `no #${id}`); fire('click', el); await settle(); await settle(); },
159
167
  clickEl: async (el) => { fire('click', el); await settle(); await settle(); },
@@ -207,6 +215,18 @@ try {
207
215
  assert.equal(lib.uploadedLabel({ from_file: ['L1', 'L2', 'L3'] }), 'uploaded · 3 lines from your doc');
208
216
  });
209
217
 
218
+ await test('the check: next line, counts only lines on the page, remembered per page and reading', () => {
219
+ assert.equal(lib.nextLineKey(READING.lines, 'L0003'), 'L0004');
220
+ assert.equal(lib.nextLineKey(READING.lines, 'L0007'), null, 'the last line has no next');
221
+ assert.equal(lib.nextLineKey(READING.lines, 'L9999'), null);
222
+ assert.equal(lib.confirmedCount(READING.lines, new Set(['L0001', 'L0004', 'L9999'])), 2);
223
+ assert.equal(lib.confirmStoreKey('landing:index', 'rh-1'), 'hall-tweak-checked:landing:index:rh-1');
224
+ assert.deepEqual([...lib.parseConfirmed('["L0002","L9999"]', READING.lines)], ['L0002'], 'a key the reading no longer has is dropped');
225
+ assert.deepEqual([...lib.parseConfirmed('off', READING.lines)], [], 'junk is nothing checked');
226
+ assert.deepEqual([...lib.parseConfirmed(null, READING.lines)], []);
227
+ assert.match(lib.hintText(3, 7), /^Shift \+ Enter checks a line and jumps to the next one\..* 3 of 7 checked\.$/);
228
+ });
229
+
210
230
  // ── the page, booted ─────────────────────────────────────────────────────
211
231
  await test('the page opens on the read: every line in page order, the saved draft in its box, notes and the accent mark', async () => {
212
232
  const b = bootEditor({ answers: { 'GET /copy-desk/pages/landing:index/draft': ok(editorRead()) } });
@@ -223,7 +243,7 @@ try {
223
243
  assert.equal(b.el('tw-name').textContent, 'Home');
224
244
  assert.equal(b.el('tw-surface').textContent, '· landing');
225
245
  assert.equal(b.el('tw-title').textContent, 'Home');
226
- assert.equal(b.el('tw-open').href, 'https://cloudbongos.com/');
246
+ assert.equal(b.el('tw-open-tab').href, 'https://cloudbongos.com/');
227
247
  assert.equal(b.el('tw-submit').disabled, false);
228
248
  assert.equal(b.lines().some((t) => t.readOnly), false);
229
249
  // Section headings, one per consecutive run.
@@ -453,14 +473,171 @@ try {
453
473
  assert.equal(b.lines().length, 7);
454
474
  });
455
475
 
476
+ await test('the check button greens a line, is remembered, and toggles back', async () => {
477
+ const b = bootEditor({ answers: { 'GET /copy-desk/pages/landing:index/draft': ok(editorRead()) } });
478
+ await b.settle();
479
+ assert.equal(b.el('tw-hint').hidden, false, 'the hint teaches Shift + Enter');
480
+ assert.match(b.el('tw-hint').textContent, /Shift \+ Enter .* 0 of 7 checked\./);
481
+ const btn = b.el('tw-doc').querySelector('[data-check="L0003"]');
482
+ await b.clickEl(btn);
483
+ const row = b.el('tw-L0003').closest('.tw-row');
484
+ assert.ok(row.classList.contains('tw-row--checked'));
485
+ assert.equal(btn.getAttribute('aria-pressed'), 'true');
486
+ assert.match(b.el('tw-hint').textContent, /1 of 7 checked\./);
487
+ assert.equal(b.store['hall-tweak-checked:landing:index:rh-1'], '["L0003"]', 'kept in the browser, per page and reading');
488
+ assert.equal(b.requests.filter((r) => r.method !== 'GET').length, 0, 'never sent to the server');
489
+ await b.clickEl(btn);
490
+ assert.equal(row.classList.contains('tw-row--checked'), false);
491
+ assert.equal(b.store['hall-tweak-checked:landing:index:rh-1'], '[]');
492
+ });
493
+
494
+ await test('Shift + Enter checks the line and lands in the next; Enter alone does nothing', async () => {
495
+ const b = bootEditor({ answers: { 'GET /copy-desk/pages/landing:index/draft': ok(editorRead()) } });
496
+ await b.settle();
497
+ const plain = b.key('tw-L0003', 'Enter', false);
498
+ assert.equal(plain.prevented, false, 'a plain Enter is left alone');
499
+ assert.equal(b.el('tw-L0003').closest('.tw-row').classList.contains('tw-row--checked'), false);
500
+ const ev = b.key('tw-L0003', 'Enter', true);
501
+ assert.equal(ev.prevented, true, 'no new line is typed');
502
+ assert.ok(b.el('tw-L0003').closest('.tw-row').classList.contains('tw-row--checked'));
503
+ assert.equal(boot.focused && boot.focused.id, 'tw-L0004', 'the cursor moves to the next line');
504
+ b.key('tw-L0007', 'Enter', true);
505
+ assert.ok(b.el('tw-L0007').closest('.tw-row').classList.contains('tw-row--checked'), 'the last line checks too');
506
+ assert.match(b.el('tw-hint').textContent, /2 of 7 checked\./);
507
+ });
508
+
509
+ await test('restore puts the page words back, takes the check off, and only shows on a changed line', async () => {
510
+ const puts = [];
511
+ const b = bootEditor({ answers: {
512
+ 'GET /copy-desk/pages/landing:index/draft': ok(editorRead()),
513
+ 'PUT /copy-desk/pages/landing:index/draft': (body) => { puts.push(body); return ok({ ok: true, outcome: 'saved' }); },
514
+ } });
515
+ await b.settle();
516
+ const doc = b.el('tw-doc');
517
+ assert.equal(doc.querySelector('[data-restore="L0004"]').hidden, false, 'the changed line offers restore');
518
+ assert.equal(doc.querySelector('[data-restore="L0003"]').hidden, true, 'an untouched line does not');
519
+ b.key('tw-L0004', 'Enter', true);
520
+ assert.ok(b.el('tw-L0004').closest('.tw-row').classList.contains('tw-row--checked'));
521
+ await b.clickEl(doc.querySelector('[data-restore="L0004"]'));
522
+ assert.equal(b.el('tw-L0004').value, 'AI agents claim tasks and ship the code.');
523
+ assert.equal(b.el('tw-L0004').closest('.tw-row').classList.contains('tw-row--checked'), false, 'restore ungreens the line');
524
+ assert.equal(b.el('tw-L0004').classList.contains('tw-line--changed'), false);
525
+ assert.equal(doc.querySelector('[data-restore="L0004"]').hidden, true);
526
+ assert.equal(b.el('tw-counter').textContent, '0 of 7 rewritten · saved');
527
+ await b.flushTimers();
528
+ assert.equal(JSON.stringify(puts), JSON.stringify([{ reading_hash: 'rh-1', lines: [] }]), 'the draft saves without it');
529
+ b.type('L0003', 'Building.');
530
+ assert.equal(doc.querySelector('[data-restore="L0003"]').hidden, false, 'typing over a line brings restore back');
531
+ });
532
+
533
+ await test('a page you cannot write has no checks, no restore and no hint', async () => {
534
+ const b = bootEditor({ answers: { 'GET /copy-desk/pages/landing:index/draft': ok(editorRead({ held: false, state: 'ready' })) } });
535
+ await b.settle();
536
+ assert.equal(b.el('tw-hint').hidden, true);
537
+ assert.equal(b.el('tw-doc').querySelectorAll('button').length, 0);
538
+ const ev = b.key('tw-L0003', 'Enter', true);
539
+ assert.equal(ev.prevented, false);
540
+ });
541
+
456
542
  // ── the page's contract with the hall ────────────────────────────────────
543
+ // ── task 1004381: a brand hole is a chip the artist types around ────────────
544
+ const HOLED = {
545
+ id: 'landing:index',
546
+ reading_hash: 'rh-2',
547
+ lines: [
548
+ L('L0001', 'contact', 'Both are how {{productName}} itself works.', 'placed', { file: 'modules/public-landing/public/contact.html', line: 4, string_id: 'c1' }),
549
+ L('L0002', 'contact', 'Plain words.', 'placed', { file: 'modules/public-landing/public/contact.html', line: 5, string_id: 'c2' }),
550
+ ],
551
+ };
552
+ const holedRead = ({ held = true } = {}) => pageStatus.composeEditor({
553
+ page: PAGE,
554
+ readingPage: HOLED,
555
+ round: { id: 56, status: 'active', holder_id: held ? ME : null, holder_login: held ? 'me' : null, holder_worktree: false, description: pages.roundTaskBody(PAGE, 1) },
556
+ builderId: ME,
557
+ });
558
+ // DOM-shaped children of a chip box, as the browser holds them.
559
+ const txt = (s) => ({ nodeType: 3, nodeValue: s });
560
+ const chip = (name) => ({ nodeType: 1, nodeName: 'SPAN', getAttribute: (k) => (k === 'data-hole' ? name : null) });
561
+
562
+ await test('brand holes: split, labelled from the pack, compared, and read back from a chip box', () => {
563
+ const brand = { identity: { productName: 'Cloud Bongos', worldName: '' }, currency: { label: 'credits' } };
564
+ assert.deepEqual(holes.splitHoles('a {{productName}} b'), [{ text: 'a ' }, { hole: 'productName' }, { text: ' b' }]);
565
+ assert.equal(holes.holeLabel('productName', brand), 'Cloud Bongos');
566
+ assert.equal(holes.holeLabel('Currency', brand), 'Credits');
567
+ assert.equal(holes.holeLabel('worldName', brand), 'worldName', 'an empty pack value names the hole rather than showing nothing');
568
+ assert.equal(holes.sameHoles('x {{productName}} y', '{{productName}}z'), true);
569
+ assert.equal(holes.sameHoles('x {{productName}} y', 'x Cloud Bongos y'), false, 'a dropped hole is not the same');
570
+ assert.equal(holes.sameHoles('{{a}}', '{{a}} {{a}}'), false, 'nor is an added one');
571
+ assert.equal(holes.holesHtml('<b>{{productName}}</b>', { brand, cls: 'tw-hole', inBox: true }),
572
+ '&lt;b&gt;<span class="tw-hole" data-hole="productName" contenteditable="false" title="A fixed name the page fills in: {{productName}}">Cloud Bongos</span>&lt;/b&gt;');
573
+ assert.equal(holes.serializeHoles([txt('This project, '), chip('productName'), txt(' has '), { nodeType: 1, nodeName: 'BR', getAttribute: () => null }, txt('two.')]),
574
+ 'This project, {{productName}} has two.', 'every typed space is kept, a chip is its token again');
575
+ // A chip that ends the box gets a zero-width caret stop after it (a browser
576
+ // cannot put the caret after a trailing non-editable chip); it never saves.
577
+ const ZW = String.fromCharCode(0x200b);
578
+ assert.ok(holes.holesHtml('Hi {{productName}}', { brand, inBox: true }).endsWith(`</span>${ZW}`));
579
+ assert.ok(holes.holesHtml('{{productName}} hi', { brand, inBox: true }).startsWith(`${ZW}<span`));
580
+ assert.equal(holes.holesHtml('Hi {{productName}}', { brand }).includes(ZW), false, 'display outside a box carries none');
581
+ assert.equal(holes.serializeHoles([txt(ZW), chip('productName'), txt(`${ZW} today`)]), '{{productName}} today');
582
+ });
583
+
584
+ await test('a line holding {{productName}} is a box with the product name as a fixed chip, and saves the token', async () => {
585
+ const puts = [];
586
+ const b = bootEditor({ answers: {
587
+ 'GET /copy-desk/pages/landing:index/draft': ok(holedRead()),
588
+ 'PUT /copy-desk/pages/landing:index/draft': (body) => { puts.push(body); return ok({ ok: true, outcome: 'saved' }); },
589
+ } });
590
+ await b.settle();
591
+ const box = b.el('tw-L0001');
592
+ assert.equal(box.tagName, 'div', 'a holed line is not a textarea');
593
+ assert.equal(box.attrs.contenteditable, 'true');
594
+ assert.equal(box.attrs.role, 'textbox');
595
+ const c = box.querySelector('.tw-hole');
596
+ assert.ok(c, 'the hole is a chip');
597
+ assert.equal(c.attrs['data-hole'], 'productName');
598
+ assert.equal(c.attrs.contenteditable, 'false', 'the chip cannot be typed into');
599
+ assert.equal(c.textContent, 'Cloud Bongos', 'it reads as the product name');
600
+ assert.ok(!/\{\{/.test(box.innerHTML.replace(/title="[^"]*"/g, '')), 'no raw token in the box');
601
+ assert.equal(b.el('tw-L0002').tagName, 'textarea', 'a line with no hole is untouched');
602
+
603
+ box.childNodes = [txt('This project, '), chip('productName'), txt(', has two doors.')];
604
+ b.fire("input", box);
605
+ assert.match(b.el('tw-L0001-note').innerHTML, /was: Both are how <span class="tw-hole" data-hole="productName"[^>]*>Cloud Bongos<\/span> itself works\./, 'the was: note shows the chip too');
606
+ await b.flushTimers();
607
+ assert.equal(JSON.stringify(puts), JSON.stringify([{ reading_hash: 'rh-2', lines: [{ key: 'L0001', after: 'This project, {{productName}}, has two doors.' }] }]),
608
+ 'the stored words keep the {{token}} and every space around it');
609
+ });
610
+
611
+ await test('deleting or adding a hole is undone on the spot and saves nothing; read-only boxes are not editable', async () => {
612
+ const puts = [];
613
+ const b = bootEditor({ answers: {
614
+ 'GET /copy-desk/pages/landing:index/draft': ok(holedRead()),
615
+ 'PUT /copy-desk/pages/landing:index/draft': (body) => { puts.push(body); return ok({ ok: true, outcome: 'saved' }); },
616
+ } });
617
+ await b.settle();
618
+ const box = b.el('tw-L0001');
619
+ box.childNodes = [txt('Both are how Cloud Bongos itself works.')];
620
+ b.fire("input", box);
621
+ assert.equal(b.pending(), 0, 'no save is scheduled');
622
+ assert.ok(box.querySelector('.tw-hole'), 'the chip is put back');
623
+ box.childNodes = [txt('Both '), chip('productName'), chip('productName')];
624
+ b.fire("input", box);
625
+ assert.equal(b.pending(), 0);
626
+ await b.flushTimers();
627
+ assert.equal(puts.length, 0);
628
+
629
+ const r = bootEditor({ answers: { 'GET /copy-desk/pages/landing:index/draft': ok(holedRead({ held: false })) } });
630
+ await r.settle();
631
+ assert.equal(r.el('tw-L0001').attrs.contenteditable, 'false', 'a page you do not hold cannot be typed into');
632
+ });
633
+
457
634
  await test('the markup: one h1, the shell\'s full-screen switch, the lib before the page, no web font', () => {
458
635
  assert.match(HTML, /<body data-page="tweak-editor"[^>]*data-fullscreen-switch/);
459
636
  assert.equal((HTML.match(/<h1\b/g) || []).length, 1);
460
637
  assert.ok(HTML.indexOf('tweak-editor-lib.js') < HTML.indexOf('tweak-editor.js?'), 'the lib loads first');
461
- assert.match(HTML, /shell\.js\?v=2026-09-28-archive/);
638
+ assert.match(HTML, /shell\.js\?v=2026-09-29-renderdeploy/);
462
639
  assert.equal(/fonts\.googleapis\.com\/css/.test(HTML), false);
463
- for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
640
+ for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-open-more', 'tw-divider', 'tw-openwrap', 'tw-open-menu', 'tw-open-tab', 'tw-open-split', 'tw-frame', 'tw-frame-view', 'tw-split-close', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
464
641
  assert.ok(new RegExp(`id="${id}"`).test(HTML), `#${id} is in the markup`);
465
642
  }
466
643
  assert.match(HTML, />← studio</);
@@ -468,6 +645,134 @@ try {
468
645
  assert.match(HTML, />back to the studio</);
469
646
  });
470
647
 
648
+ await test('no visible page title or sub text on the paper; the h1 stays for screen readers', () => {
649
+ assert.match(HTML, /<h1 class="tw-sr" id="tw-title">/);
650
+ assert.equal(/tw-intro|tw-head/.test(HTML), false, 'the intro sentence and its block are gone');
651
+ assert.equal(/Every word on the page, in the order it shows them/.test(HTML), false);
652
+ });
653
+
654
+ await test('split view can frame only its own host; the live site refuses to be framed from another', () => {
655
+ const here = { origin: 'https://builders.cloudbongos.com', href: 'https://builders.cloudbongos.com/builders/tweak-editor' };
656
+ assert.equal(lib.canFrame('https://builders.cloudbongos.com/studio', here), true);
657
+ assert.equal(lib.canFrame('https://cloudbongos.com/', here), false, 'the apex is another origin');
658
+ assert.equal(lib.canFrame('not a url', { origin: '', href: '' }), false);
659
+ assert.equal(lib.splitWindowFeatures({ availWidth: 1600, availHeight: 1000, availLeft: 0, availTop: 0 }), 'popup=yes,width=1600,height=500,left=0,top=500', 'the bottom half of the screen');
660
+ assert.match(lib.splitWindowFeatures(undefined), /^popup=yes,width=\d+,height=\d+,left=0,top=\d+$/);
661
+ });
662
+
663
+ await test('the split button and the divider: last choice, and a size kept between 20% and 80%', () => {
664
+ assert.equal(lib.openChoice('split'), 'split');
665
+ assert.equal(lib.openChoice('off'), 'tab', 'anything else is a new tab');
666
+ assert.equal(lib.openChoice(null), 'tab');
667
+ assert.equal(lib.OPEN_WORDS.tab, 'open the page ↗');
668
+ assert.equal(lib.OPEN_WORDS.split, 'open in split view');
669
+ assert.equal(lib.clampSplit(null), 0.5);
670
+ assert.equal(lib.clampSplit('off'), 0.5);
671
+ assert.equal(lib.clampSplit('0.95'), 0.8);
672
+ assert.equal(lib.clampSplit(0.05), 0.2);
673
+ assert.equal(lib.clampSplit('0.333'), 0.33);
674
+ assert.equal(lib.splitFromPointer(700, 1000), 0.3, 'the page takes what lies below the pointer');
675
+ assert.equal(lib.splitFromPointer(0, 1000), 0.8);
676
+ assert.equal(lib.splitFromPointer(500, 0), 0.5);
677
+ assert.equal(lib.splitFromKey(0.5, 'ArrowUp'), 0.55);
678
+ assert.equal(lib.splitFromKey(0.5, 'ArrowDown'), 0.45);
679
+ assert.equal(lib.splitFromKey(0.8, 'ArrowUp'), 0.8);
680
+ assert.equal(lib.splitFromKey(0.5, 'Home'), 0.2);
681
+ assert.equal(lib.splitFromKey(0.5, 'End'), 0.8);
682
+ assert.equal(lib.splitFromKey(0.5, 'x'), null);
683
+ });
684
+
685
+ await test('open the page: the arrow opens the menu, and the main button repeats the last choice', async () => {
686
+ const saved = PAGE.url;
687
+ PAGE.url = 'https://builders.{apex}/studio';
688
+ try {
689
+ const b = bootEditor({ answers: { 'GET /copy-desk/pages/landing:index/draft': ok(editorRead()) } });
690
+ await b.settle();
691
+ assert.equal(b.el('tw-open').textContent, 'open the page ↗', 'a new tab by default');
692
+ assert.equal(b.el('tw-open-tab').attrs.target, '_blank');
693
+ assert.equal(b.el('tw-frame').hidden, true, 'no frame until asked');
694
+ assert.ok(!b.el('tw-frame-view').src, 'and nothing loaded');
695
+ await b.click('tw-open');
696
+ assert.deepEqual(b.opened.map((a) => a.slice(0, 2)), [['https://builders.cloudbongos.com/studio', '_blank']], 'the main button opens a new tab');
697
+ assert.equal(b.el('tw-openwrap').classList.contains('is-open'), false, 'and no menu');
698
+ await b.click('tw-open-more');
699
+ assert.ok(b.el('tw-openwrap').classList.contains('is-open'), 'the arrow opens the menu');
700
+ assert.equal(b.el('tw-open-more').getAttribute('aria-expanded'), 'true');
701
+ await b.click('tw-open-split');
702
+ assert.equal(b.el('tw-frame').hidden, false);
703
+ assert.equal(b.el('tw-frame-view').src, 'https://builders.cloudbongos.com/studio', 'the live page, in place');
704
+ assert.equal(b.el('tw-open-split').textContent, 'close split view');
705
+ assert.equal(b.el('tw-openwrap').classList.contains('is-open'), false, 'the menu closes after a choice');
706
+ assert.equal(b.el('tw-open').textContent, 'open in split view', 'the main button now repeats the split');
707
+ assert.equal(b.store['hall-tweak-open-as'], 'split');
708
+ assert.equal(b.store['hall-tweak-split'], 'on');
709
+ await b.click('tw-split-close');
710
+ assert.equal(b.el('tw-frame').hidden, true);
711
+ assert.equal(b.el('tw-frame-view').src, 'about:blank', 'closing unloads the page');
712
+ assert.equal(b.store['hall-tweak-split'], 'off');
713
+ await b.click('tw-open');
714
+ assert.equal(b.el('tw-frame').hidden, false, 'the main button reopens the split');
715
+ assert.equal(b.opened.length, 1, 'no second window for a hall page');
716
+ assert.equal(b.requests.filter((r) => r.method !== 'GET').length, 0, 'no write');
717
+ } finally { PAGE.url = saved; }
718
+ });
719
+
720
+ await test('the divider resizes by drag and by arrow keys, and the size is remembered', async () => {
721
+ const saved = PAGE.url;
722
+ PAGE.url = 'https://builders.{apex}/studio';
723
+ try {
724
+ const b = bootEditor({ answers: { 'GET /copy-desk/pages/landing:index/draft': ok(editorRead()) } });
725
+ await b.settle();
726
+ await b.click('tw-open-more');
727
+ await b.click('tw-open-split');
728
+ const div = b.el('tw-divider');
729
+ assert.equal(div.getAttribute('aria-valuenow'), '50');
730
+ b.fire('pointerdown', div, { clientY: 500, preventDefault() {} });
731
+ assert.ok(b.el('tw-frame').classList.contains('is-dragging'), 'the frame lets the pointer through while dragging');
732
+ b.fire('pointermove', div, { clientY: 700 });
733
+ assert.equal(div.getAttribute('aria-valuenow'), '30');
734
+ b.fire('pointerup', div, {});
735
+ assert.equal(b.el('tw-frame').classList.contains('is-dragging'), false);
736
+ assert.equal(b.store['hall-tweak-split-size'], '0.3');
737
+ b.fire('pointermove', div, { clientY: 100 });
738
+ assert.equal(div.getAttribute('aria-valuenow'), '30', 'moving after the drag ends changes nothing');
739
+ const ev = b.key('tw-divider', 'ArrowUp', false);
740
+ assert.equal(ev.prevented, true);
741
+ assert.equal(div.getAttribute('aria-valuenow'), '35');
742
+ assert.equal(b.store['hall-tweak-split-size'], '0.35');
743
+ } finally { PAGE.url = saved; }
744
+ });
745
+
746
+ await test('the next page opens the way the last one was left', async () => {
747
+ const saved = PAGE.url;
748
+ PAGE.url = 'https://builders.{apex}/studio';
749
+ try {
750
+ const b = bootEditor({
751
+ answers: { 'GET /copy-desk/pages/landing:index/draft': ok(editorRead()) },
752
+ stored: { 'hall-tweak-open-as': 'split', 'hall-tweak-split': 'on', 'hall-tweak-split-size': '0.4' },
753
+ });
754
+ await b.settle();
755
+ assert.equal(b.el('tw-frame').hidden, false, 'split view is back');
756
+ assert.equal(b.el('tw-divider').getAttribute('aria-valuenow'), '40', 'at its size');
757
+ assert.equal(b.el('tw-open').textContent, 'open in split view');
758
+ } finally { PAGE.url = saved; }
759
+ });
760
+
761
+ await test('a page on the apex splits into its own window docked to the bottom half, and never on load', async () => {
762
+ const b = bootEditor({
763
+ answers: { 'GET /copy-desk/pages/landing:index/draft': ok(editorRead()) },
764
+ stored: { 'hall-tweak-open-as': 'split', 'hall-tweak-split': 'on' },
765
+ });
766
+ await b.settle();
767
+ assert.equal(b.opened.length, 0, 'no popup on load: a browser would block it');
768
+ assert.equal(b.el('tw-frame').hidden, true, 'nothing framed: the site would refuse');
769
+ await b.click('tw-open');
770
+ assert.equal(b.opened.length, 1);
771
+ assert.deepEqual(b.opened[0].slice(0, 2), ['https://cloudbongos.com/', 'tw-live-page']);
772
+ assert.equal(b.opened[0][2], 'popup=yes,width=1600,height=500,left=0,top=500');
773
+ assert.equal(b.el('tw-frame').hidden, true);
774
+ });
775
+
471
776
  await test('the page writes only the copy-desk routes it names', () => {
472
777
  const js = read('tweak-editor.js');
473
778
  const writes = [...js.matchAll(/api\.request\('(POST|PUT|PATCH|DELETE)', ([^,]+),/g)].map((m) => `${m[1]} ${m[2]}`);