@bongos/core 1.19.1080 → 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 (258) hide show
  1. package/.bongos-core.json +573 -188
  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 +34 -0
  5. package/clients/bongos-client/index.cjs +34 -0
  6. package/clients/bongos-client/index.d.ts +53 -4
  7. package/clients/bongos-client/index.mjs +34 -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/0343-a-module-score-is-a-security-gate-then-an-average-of-visible-parts.md +2 -0
  14. package/docs/adr/0347-every-store-module-ships-a-how-to.md +135 -0
  15. package/docs/adr/0348-the-web-tier-may-look-read-only-at-what-an-owners-render-key-can-see.md +53 -0
  16. package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +106 -0
  17. package/docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md +103 -0
  18. package/docs/adr/README.md +4 -0
  19. package/docs/api/openapi.json +995 -40
  20. package/docs/api-reference.md +33 -8
  21. package/docs/architecture.md +6 -2
  22. package/docs/copy-inventory.md +620 -553
  23. package/docs/copy-registry.json +1467 -820
  24. package/docs/file-map.md +2 -0
  25. package/docs/module-api-changelog.md +8 -0
  26. package/docs/modules-contract.md +1 -0
  27. package/docs/page-inventory.json +38 -4
  28. package/docs/page-readings.json +1674 -1566
  29. package/migrations/core_260_goals_working_area.sql +63 -0
  30. package/migrations/core_261_grade_attempts_verdicts.sql +24 -0
  31. package/migrations/core_262_drop_builder_box_blocked.sql +33 -0
  32. package/modules/agents/lib/validate.js +43 -0
  33. package/modules/autonomy/gauge.js +38 -2
  34. package/modules/grading/grader-subagent.js +37 -2
  35. package/modules/grading/grader-workers/reader.js +65 -5
  36. package/modules/hall-ui/public/approval-queue.css +33 -7
  37. package/modules/hall-ui/public/approval-queue.js +9 -2
  38. package/modules/hall-ui/public/atlas.html +1 -1
  39. package/modules/hall-ui/public/blockers.html +1 -1
  40. package/modules/hall-ui/public/board-room.html +1 -1
  41. package/modules/hall-ui/public/brand-holes.js +121 -0
  42. package/modules/hall-ui/public/collab.html +1 -1
  43. package/modules/hall-ui/public/collab.js +1 -1
  44. package/modules/hall-ui/public/copy-desk.html +1 -1
  45. package/modules/hall-ui/public/deploy.html +6 -1
  46. package/modules/hall-ui/public/deploy.js +22 -8
  47. package/modules/hall-ui/public/diagrams.html +1 -1
  48. package/modules/hall-ui/public/dom-utils.js +20 -0
  49. package/modules/hall-ui/public/drachmae.html +1 -1
  50. package/modules/hall-ui/public/fleet.html +1 -1
  51. package/modules/hall-ui/public/gate.html +1 -1
  52. package/modules/hall-ui/public/goals.html +1 -1
  53. package/modules/hall-ui/public/government.html +1 -1
  54. package/modules/hall-ui/public/idea.html +1 -1
  55. package/modules/hall-ui/public/idea.js +28 -0
  56. package/modules/hall-ui/public/ideas.html +1 -1
  57. package/modules/hall-ui/public/ideas.js +19 -3
  58. package/modules/hall-ui/public/index.html +5 -1
  59. package/modules/hall-ui/public/modules.html +1 -1
  60. package/modules/hall-ui/public/primer.html +1 -1
  61. package/modules/hall-ui/public/profile-nudge.js +21 -4
  62. package/modules/hall-ui/public/profile.html +1 -1
  63. package/modules/hall-ui/public/profile.js +1 -1
  64. package/modules/hall-ui/public/project-settings.html +1 -1
  65. package/modules/hall-ui/public/ranks.html +1 -1
  66. package/modules/hall-ui/public/roadmap.html +1 -1
  67. package/modules/hall-ui/public/roster.html +1 -1
  68. package/modules/hall-ui/public/sessions.html +1 -1
  69. package/modules/hall-ui/public/settings.html +19 -30
  70. package/modules/hall-ui/public/settings.js +24 -177
  71. package/modules/hall-ui/public/settings.states.json +1 -1
  72. package/modules/hall-ui/public/shell.js +4 -2
  73. package/modules/hall-ui/public/studio.css +14 -8
  74. package/modules/hall-ui/public/studio.html +5 -4
  75. package/modules/hall-ui/public/task.html +1 -1
  76. package/modules/hall-ui/public/thinking.css +6 -3
  77. package/modules/hall-ui/public/thinking.html +1 -1
  78. package/modules/hall-ui/public/tweak-editor-lib.js +82 -1
  79. package/modules/hall-ui/public/tweak-editor.css +142 -11
  80. package/modules/hall-ui/public/tweak-editor.html +25 -9
  81. package/modules/hall-ui/public/tweak-editor.js +243 -10
  82. package/modules/hall-ui/public/watch.html +1 -1
  83. package/modules/hall-ui/public/work.html +1 -1
  84. package/modules/hall-ui/records/approval-queue.md +6 -0
  85. package/modules/hall-ui/records/tweak-editor.md +6 -0
  86. package/modules/ideas/ratify.js +36 -8
  87. package/modules/ideas/routes/ratify-goal.js +6 -0
  88. package/modules/lifecycle/db-analytics.js +79 -10
  89. package/modules/lifecycle/db-claims.js +48 -1
  90. package/modules/lifecycle/db-deps-criteria.js +94 -40
  91. package/modules/lifecycle/db-goals.js +4 -1
  92. package/modules/lifecycle/db-grade.js +9 -1
  93. package/modules/lifecycle/db-relevance-flags.js +183 -0
  94. package/modules/lifecycle/db-versions.js +4 -3
  95. package/modules/lifecycle/db.js +19 -0
  96. package/modules/lifecycle/done-when.js +5 -3
  97. package/modules/lifecycle/est-advisory.js +31 -4
  98. package/modules/lifecycle/goal-task-relevance-advisory.js +153 -0
  99. package/modules/lifecycle/goal-task-relevance-judge.js +226 -0
  100. package/modules/lifecycle/migrations/lifecycle_014_goal_task_relevance_flags.sql +82 -0
  101. package/modules/lifecycle/module.json +2 -1
  102. package/modules/lifecycle/routes/claims.js +8 -1
  103. package/modules/lifecycle/routes/goal-task-relevance.js +72 -0
  104. package/modules/lifecycle/routes/goals.js +1 -1
  105. package/modules/lifecycle/routes/tasks.js +27 -24
  106. package/modules/lifecycle/routes/visuals.js +9 -0
  107. package/modules/lifecycle/ship-preflight.js +7 -11
  108. package/modules/lifecycle/task-visuals.js +109 -5
  109. package/modules/npm-release/module.json +2 -1
  110. package/modules/npm-release/preview/commands.js +81 -0
  111. package/modules/npm-release/preview/divert.js +95 -0
  112. package/modules/npm-release/preview/env.js +78 -0
  113. package/modules/npm-release/preview/proxy.js +107 -0
  114. package/modules/npm-release/preview/runtime.js +83 -0
  115. package/modules/npm-release/preview/supervisor.js +175 -0
  116. package/modules/npm-release/public/work.js +65 -1
  117. package/modules/npm-release/routes/preview.js +133 -0
  118. package/modules/provisioning/app-status.js +83 -0
  119. package/modules/provisioning/migrations/provisioning_028_render_standup.sql +46 -0
  120. package/modules/provisioning/module.json +4 -2
  121. package/modules/provisioning/pollers/app-liveness.js +110 -0
  122. package/modules/provisioning/provisioning.js +5 -5
  123. package/modules/provisioning/render-lookup.js +68 -0
  124. package/modules/provisioning/render-standup.js +134 -0
  125. package/modules/provisioning/routes/render-standup.js +165 -0
  126. package/modules/provisioning/starter-bundles.js +4 -1
  127. package/modules/public-landing/public/account-private.states.json +14 -0
  128. package/modules/public-landing/public/account.html +513 -0
  129. package/modules/public-landing/public/account.probes.json +37 -0
  130. package/modules/public-landing/public/account.states.json +16 -0
  131. package/modules/public-landing/public/index.html +5 -2
  132. package/modules/public-landing/public/projects.html +661 -18
  133. package/modules/public-landing/public/projects.states.json +8 -1
  134. package/modules/render-deploy/deploys.js +203 -0
  135. package/modules/render-deploy/migrations/render_deploy_001_app.sql +22 -0
  136. package/modules/render-deploy/module.json +25 -0
  137. package/modules/render-deploy/public/deploy.css +10 -0
  138. package/modules/render-deploy/public/deploy.js +249 -0
  139. package/modules/render-deploy/render.js +56 -0
  140. package/modules/render-deploy/routes/act.js +88 -0
  141. package/modules/render-deploy/routes/app.js +65 -0
  142. package/modules/render-deploy/routes/history.js +38 -0
  143. package/modules/specialities/db.js +27 -8
  144. package/modules/specialities/migrations/specialities_003_skills.sql +46 -0
  145. package/modules/specialities/routes/specialities.js +51 -3
  146. package/modules/specialities/skills.js +94 -0
  147. package/modules/specialities/specialities.js +56 -9
  148. package/modules/ui-design/kit/fixtures/me-cross-project-private.json +12 -0
  149. package/modules/ui-design/kit/fixtures/me__cross-project.json +15 -0
  150. package/modules/ui-design/kit/fixtures/provisioning-instance-render.json +39 -0
  151. package/modules/ui-design/kit/lib.js +3 -1
  152. package/modules/ui-design/kit/serve.js +96 -0
  153. package/package-lock.json +2 -2
  154. package/package.json +1 -1
  155. package/release-notes.json +173 -0
  156. package/scripts/gds/agents-sync.js +13 -3
  157. package/scripts/gds/autobongos-run.js +80 -2
  158. package/scripts/gds/autobongos-service.cmd +12 -0
  159. package/scripts/gds/copy-apply.js +14 -0
  160. package/scripts/gds/dev-box-guard.js +3 -2
  161. package/scripts/gds/fitness.js +9 -0
  162. package/scripts/gds/grade-correlation-audit.js +42 -12
  163. package/scripts/gds/grade-replay.js +1 -1
  164. package/scripts/gds/provision-core-upgrade.js +38 -7
  165. package/scripts/gds/provision-pin-key.js +176 -0
  166. package/scripts/gds/provision-render.js +189 -0
  167. package/scripts/gds/provision-teardown.js +9 -2
  168. package/scripts/gds/provision-units.js +31 -0
  169. package/scripts/gds/provision.js +12 -12
  170. package/scripts/gds/publish-manifest.js +1 -0
  171. package/scripts/gds/render-api.js +197 -0
  172. package/scripts/gds/render-payload.js +84 -0
  173. package/scripts/gds/run-unit-tests.js +10 -0
  174. package/scripts/gds/ship-finish.js +19 -20
  175. package/scripts/gds/smoke-dependencies.sh +37 -8
  176. package/scripts/gds/status.js +12 -4
  177. package/scripts/gds/upgrade.js +2 -2
  178. package/scripts/public-mirror-export.js +7 -1
  179. package/src/bongos/module-scope-map.js +13 -0
  180. package/src/bongos/serve-internal.js +3 -1
  181. package/src/module-api.js +10 -1
  182. package/src/platform-server.js +39 -0
  183. package/tests/account_privacy_flags.mjs +10 -6
  184. package/tests/agents_authoring.mjs +15 -1
  185. package/tests/agents_sync.mjs +106 -5
  186. package/tests/agents_validate.mjs +45 -0
  187. package/tests/api_path_404.mjs +6 -0
  188. package/tests/autobongos_cadence.mjs +7 -0
  189. package/tests/autobongos_loop.mjs +136 -1
  190. package/tests/autonomy_gauge.mjs +62 -0
  191. package/tests/blocker_hall_live_proof.mjs +275 -0
  192. package/tests/claim_gate_ci_unblock.mjs +116 -0
  193. package/tests/claim_gate_rebase.mjs +15 -3
  194. package/tests/collab_page.mjs +10 -0
  195. package/tests/conductor_main_e2e.mjs +97 -0
  196. package/tests/core_262_drop_box_blocked_db.mjs +143 -0
  197. package/tests/core_upgrade_runner.mjs +1 -0
  198. package/tests/dependency_writes_atomic.mjs +191 -0
  199. package/tests/deploy_page_projects.mjs +2 -1
  200. package/tests/effective_visibility_predicate.mjs +15 -0
  201. package/tests/est_advisory.mjs +27 -0
  202. package/tests/goal_map_page.mjs +26 -0
  203. package/tests/goal_task_relevance.mjs +518 -0
  204. package/tests/goal_working_area.mjs +146 -0
  205. package/tests/grade_attempts.mjs +61 -0
  206. package/tests/grade_attribution.mjs +23 -0
  207. package/tests/grade_correlation_audit.mjs +54 -1
  208. package/tests/grader_reader_lens.mjs +55 -2
  209. package/tests/grader_root_outage.mjs +6 -1
  210. package/tests/grader_subagent_tools_arg.mjs +102 -0
  211. package/tests/hall_approval_queue.mjs +54 -2
  212. package/tests/hall_settings_world.mjs +3 -1
  213. package/tests/hall_tweak_editor.mjs +316 -11
  214. package/tests/hub_account_page.mjs +394 -0
  215. package/tests/idea_detail_page.mjs +64 -0
  216. package/tests/idea_goal_ratification.mjs +64 -0
  217. package/tests/idea_spark_hall.mjs +60 -9
  218. package/tests/ideator_full_idea_shapes_space_proof.mjs +1 -1
  219. package/tests/landing_page.mjs +6 -3
  220. package/tests/module-scope-map.mjs +31 -8
  221. package/tests/module_api.mjs +1 -0
  222. package/tests/module_loader.mjs +1 -1
  223. package/tests/nav_permission_atoms.mjs +1 -1
  224. package/tests/no_phantom_mirror_workflow.mjs +24 -0
  225. package/tests/npm_release_preview_commands.mjs +89 -0
  226. package/tests/npm_release_preview_divert.mjs +164 -0
  227. package/tests/npm_release_preview_env.mjs +100 -0
  228. package/tests/npm_release_preview_proxy.mjs +162 -0
  229. package/tests/npm_release_preview_routes.mjs +208 -0
  230. package/tests/npm_release_preview_supervisor.mjs +203 -0
  231. package/tests/pin_write_key.mjs +383 -0
  232. package/tests/planning_session_skill.mjs +27 -0
  233. package/tests/platform_boot.mjs +71 -0
  234. package/tests/profile_nudge_links.mjs +86 -0
  235. package/tests/profile_ui_cross_project.mjs +1 -1
  236. package/tests/projects_hub.mjs +2 -2
  237. package/tests/projects_hub_app_status.mjs +249 -0
  238. package/tests/projects_hub_app_step.mjs +282 -52
  239. package/tests/projects_hub_render_connect.mjs +295 -0
  240. package/tests/provision_render.mjs +362 -0
  241. package/tests/provision_settings_apply.mjs +62 -0
  242. package/tests/provisioning_app_status.mjs +204 -0
  243. package/tests/provisioning_render_route.mjs +336 -0
  244. package/tests/provisioning_settings_apply.mjs +1 -1
  245. package/tests/provisioning_teardown_intent.mjs +2 -2
  246. package/tests/render_api.mjs +153 -0
  247. package/tests/render_check.mjs +2 -1
  248. package/tests/render_deploy.mjs +495 -0
  249. package/tests/ship_preflight.mjs +61 -8
  250. package/tests/smoke_dependencies_witness.mjs +46 -0
  251. package/tests/speciality_skills.mjs +214 -0
  252. package/tests/studio_room_height.mjs +70 -0
  253. package/tests/task_detail_includes.mjs +10 -0
  254. package/tests/task_visuals_instance_root.mjs +150 -0
  255. package/tests/tweak_batch_apply.mjs +21 -0
  256. package/tests/ui_design_kit.mjs +36 -4
  257. package/tests/upgrade.mjs +1 -1
  258. package/tests/upgrade_persist_pin.mjs +24 -0
@@ -5,6 +5,9 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Settings — {{worldName}}</title>
7
7
  <meta name="description" content="Your preferences — connections, access keys, skill voices, and sounds.">
8
+ <!-- the hub's origin, stamped server-side (task 1004259): the Profile and
9
+ privacy row links to its account page. Empty on an instance with no hub. -->
10
+ <meta name="hub-origin" content="{{hubOrigin}}">
8
11
  <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
12
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
13
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -88,37 +91,23 @@
88
91
  <p class="section-note">Saves when you press Save; shows everywhere at once.</p>
89
92
  </section>
90
93
 
91
- <!-- Cross-project record (platform-identity module, ADR 0171 / task 1002279).
92
- Revealed only when the module's own endpoint answers; hidden on a
93
- vanilla instance. P04 later moves this into the Profile rail group. -->
94
- <section class="scroll" id="cross-project-scroll" aria-labelledby="cross-project-h" data-group="account" hidden>
95
- <h2 id="cross-project-h" class="scroll__h">Cross-project record</h2>
94
+ <!-- PROFILE AND PRIVACY — one link out to the hub's account page
95
+ (task 1004259, ADR 0337 D4.3). Handle, bio, links, public/private,
96
+ recruiters and the hide-my-numbers switch belong to the platform
97
+ account, so they are edited once on the hub; this hall carried the
98
+ privacy half as two sections ("Cross-project record", "Profile
99
+ privacy") and now only points there. settings.js sets the href from
100
+ the hub-origin meta and reveals the row; with no hub (a vanilla
101
+ instance) it stays hidden. -->
102
+ <section class="scroll" id="account-hub-scroll" aria-labelledby="account-hub-h" data-group="account" hidden>
103
+ <h2 id="account-hub-h" class="scroll__h">Profile and privacy</h2>
96
104
  <p class="scroll__lede">
97
- Your track record across every project you build on, shown on your
98
- public profile. Hiding it takes effect immediately everywhere —
99
- your profile, the recruiting directory, and a recruiter's look-up of one builder.
105
+ Your handle, bio and links, whether your profile page is public,
106
+ whether recruiters can find you, and whether your record shows are
107
+ part of your account across every project — so you change them in
108
+ one place, on your account page.
100
109
  </p>
101
- <div id="cross-project-rows"></div>
102
- <p class="settings-status" id="cross-project-status" role="status" aria-live="polite"></p>
103
- </section>
104
-
105
- <!-- Profile privacy (platform-identity module, privacy spec D1 / task
106
- 1002966). The same own-scoped GET/PATCH /me/cross-project as the
107
- section above, so it reveals and hides with it; P04 later moves
108
- both into the Profile rail group. -->
109
- <section class="scroll" id="privacy-scroll" aria-labelledby="privacy-h" data-group="account" hidden>
110
- <h2 id="privacy-h" class="scroll__h">Profile privacy</h2>
111
- <p class="scroll__lede">
112
- Who can find you. Your profile page can be public or private, and
113
- recruiters looking for builders are allowed in separately — so a
114
- private profile can still be reachable for work, and a public one
115
- can stay out of recruiting entirely.
116
- </p>
117
- <div id="privacy-rows"></div>
118
- <p class="settings-status" id="privacy-status" role="status" aria-live="polite"></p>
119
- <p class="section-note">Your profile page setting applies now — private already answers "not found"
120
- everywhere your profile is read. Recruiter discoverability applies now too — switching
121
- it off removes you from both recruiting surfaces immediately, and back on puts you there again.</p>
110
+ <div class="row-end"><a class="pbtn" id="account-hub-link" href="/account">Open your account page</a></div>
122
111
  </section>
123
112
 
124
113
  <!-- CRAFT — discipline preferences (GET /me, PATCH /me/disciplines). -->
@@ -407,7 +396,7 @@
407
396
 
408
397
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
409
398
  chrome exists before the page scripts run. -->
410
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
399
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
411
400
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
412
401
  <script src="/builders/palette.js" defer></script>
413
402
  <!-- shared frontend toolkit (window.OTB) — loaded before consuming IIFEs. -->
@@ -485,179 +485,23 @@
485
485
  }
486
486
  }
487
487
 
488
- // ---- section: CROSS-PROJECT RECORD (hide flag — ADR 0171 D4, task 1002279)
488
+ // ---- section: PROFILE AND PRIVACY — a link out to the hub (task 1004259)
489
489
  //
490
- // GET/PATCH /api/bongos/me/cross-project (platform-identity module — hub only).
491
- // One on/off toggle: "show my cross-project record on my public profile".
492
- // Enforcement is server-side in the module's read functions, so flipping it
493
- // takes effect immediately on the profile and /scouting. The section
494
- // reveals ONLY when the module's endpoint answers (404 on a vanilla instance →
495
- // stays hidden, no error state). P04 later re-homes this in the Profile rail
496
- // group; storage is account-global on the hub (not behavior_prefs).
497
- function renderCrossProject(data) {
498
- const wrap = $('#cross-project-rows');
499
- if (!wrap) return;
500
- const hidden = !!(data && data.hide_stats);
501
- const on = !hidden; // the toggle reads as "shown"
502
- const cp = data && data.cross_project;
503
- const totals = cp && cp.totals ? cp.totals : null;
504
- const totalsLine = totals
505
- ? `Your record: ${totals.projects} project${totals.projects === 1 ? '' : 's'} · ` +
506
- `${totals.credits} credits · ${totals.works_shipped} works shipped · ${totals.karma} karma.`
507
- : 'No cross-project activity reported yet.';
508
- const consentNote = data && data.profile_state !== 'active'
509
- ? ' Your profile publishes after your next sign-in at the hub.'
510
- : '';
511
- wrap.innerHTML =
512
- '<div class="sound-row" data-cross-project="hide">' +
513
- '<div class="sound-row__text">' +
514
- '<div class="sound-row__label">Show my cross-project record</div>' +
515
- `<div class="sound-row__desc">${escapeHtml(totalsLine + consentNote)} You always see your own numbers here.</div>` +
516
- '</div>' +
517
- switchHtml(on, 'Show my cross-project record') +
518
- '</div>';
519
- }
520
-
521
- // ---- section: PROFILE PRIVACY (account_visibility + recruiter_discoverable
522
- // — privacy spec D1/D3, task 1002966)
523
- //
524
- // Two more rows on the SAME own-scoped GET/PATCH /me/cross-project, because
525
- // both flags live on the hub account beside hide_stats (never behavior_prefs).
526
- // The recruiter row's copy must name EXACTLY what being discoverable grants —
527
- // the spec's D3 sliver — and that grant shrinks to name-only when the
528
- // cross-project record is hidden, because the rest of the sliver is derived
529
- // from it. `recruiter_discoverable` is tri-state server-side: null means the
530
- // answer follows the visibility setting, which the row says out loud. The
531
- // toggle is deliberately BINARY (spec D1: "one column, one settings toggle");
532
- // resetting back to null stays an API affordance, not a third UI state.
533
- //
534
- // COPY RULE — a toggle may promise exactly what is enforced, and BOTH rows now
535
- // are, one task apart. VISIBILITY went live with task 1002967: every public
536
- // read joins the shared effective-visibility predicate, so private really does
537
- // 404 — including to the owner, which the row must say out loud: a page that
538
- // vanishes for you too is surprising unless you are told where your record went
539
- // (here, Settings). DISCOVERABILITY went live in two halves: task 1002290 (the People directory)
540
- // and task 1002968 (the D3 pre-accept sliver). BOTH recruiting reads now join
541
- // the reach axis — listScoutingRollup via scoutingListableSql, the sliver via
542
- // recruiterSliverVisibleSql — so the row may finally name both, which is exactly
543
- // what 1002290 narrowed it away from while the second surface was unbuilt. The
544
- // wider claim is CHECKABLE rather than open-ended: account_privacy_flags.mjs
545
- // pins that the recruiting surface has exactly those two reads and that each one
546
- // joins reach, so a third would red the guard before this sentence could quietly
547
- // become false.
548
- function renderPrivacy(data) {
549
- const wrap = $('#privacy-rows');
550
- if (!wrap) return;
551
- const isPublic = !data || data.account_visibility !== 'private';
552
- const discoverable = !!(data && data.recruiter_discoverable_effective);
553
- const follows = !data || data.recruiter_discoverable == null;
554
- const grant = data && data.hide_stats
555
- ? 'Because your cross-project record is hidden, they will see your name and your picture, and nothing else.'
556
- : 'They will see your name, your picture, your roles, and whether you have been active in the last 30 days — never your credits, works shipped, karma, or the projects you build on.';
557
- // The surprising half, and only where it applies: a private profile answers
558
- // "not found" to every visitor, and this one switch is the exception.
559
- const despitePrivate = isPublic ? ''
560
- : ' This is the one thing a recruiter can see while your profile page is private.';
561
- wrap.innerHTML =
562
- privacyRow('visibility', 'Public profile page', isPublic ? 'Public' : 'Private', isPublic,
563
- 'Public means anyone can open your profile page and find you by name. Private hides the page from everyone — including you: it answers "not found" to every visitor, and you read your own record here in Settings instead.', '') +
564
- privacyRow('discoverable', 'Discoverable by recruiters', discoverable ? 'On' : 'Off', discoverable,
565
- `Recruiters looking for builders can find you. ${grant}${despitePrivate} Turning this off removes you from both recruiting surfaces — the recruiting directory, and a recruiter's look-up of one builder — straight away.`,
566
- follows ? ' <span class="voices__default">· follows your profile setting</span>' : '');
567
- }
568
-
569
- // One row of the privacy section, in the same sound-row markup every toggle on
570
- // this page uses. `note` is trusted markup (a caller-supplied span), the label
571
- // and description are escaped.
572
- function privacyRow(key, label, state, on, desc, note) {
573
- return `<div class="sound-row" data-privacy="${key}">` +
574
- '<div class="sound-row__text">' +
575
- `<div class="sound-row__label">${escapeHtml(label)}<span class="prow__tag">${escapeHtml(state)}</span>${note}</div>` +
576
- `<div class="sound-row__desc">${escapeHtml(desc)}</div>` +
577
- '</div>' +
578
- switchHtml(on, `${label}: ${state}`) +
579
- '</div>';
580
- }
581
-
582
- // The status/toast line for a privacy toggle. Deliberately says only what the
583
- // flag itself changed — the exact grant lives in the row's description, which
584
- // re-renders from the server right after the save.
585
- function privacyMsg(key, on) {
586
- if (key === 'visibility') {
587
- return on
588
- ? 'Saved — your profile page is set to public.'
589
- : 'Saved — your profile page is set to private.';
590
- }
591
- return on
592
- ? 'Saved — recruiters can find you.'
593
- : 'Saved — you are out of the recruiting surfaces.';
594
- }
595
-
596
- // PATCH any own-scoped privacy flag (routes/profile.js): { hide_stats } and/or
597
- // { account_visibility, recruiter_discoverable }. Always re-reads afterwards —
598
- // the effective recruiter answer is derived server-side, so the server's own
599
- // view is the only state worth rendering.
600
- async function savePrivacy(body, statusSel, msg, btn) {
601
- const status = $(statusSel);
602
- if (status) status.textContent = 'Saving…';
603
- if (btn) btn.disabled = true;
604
- try {
605
- const res = await api.request('PATCH', `${API}/me/cross-project`, { body });
606
- if (res.status === 401) { toAuth(); return; }
607
- if (!res.ok) throw new Error(`cross-project PATCH → ${res.status}`);
608
- await loadCrossProject(); // re-render from the server's truth (flags + record)
609
- if (status) status.textContent = msg;
610
- toast(msg);
611
- } catch (err) {
612
- console.error('[settings] save privacy failed', err);
613
- if (status) status.textContent = 'Could not save. Try again.';
614
- toast('Could not save your privacy preference. Try again.');
615
- if (btn) btn.disabled = false;
616
- }
617
- }
618
-
619
- // The hide flag. Body: { hide_stats: <bool> } (routes/profile.js).
620
- function saveCrossProject(hide, btn) {
621
- return savePrivacy({ hide_stats: hide }, '#cross-project-status', hide
622
- ? 'Cross-project record hidden — removed from your profile and directories immediately.'
623
- : 'Cross-project record shown on your public profile.', btn);
624
- }
625
-
626
- let crossProjectWired = false;
627
- async function loadCrossProject() {
628
- const section = $('#cross-project-scroll');
629
- if (!section) return;
630
- try {
631
- const res = await api.request('GET', `${API}/me/cross-project`);
632
- if (res.status === 401) { toAuth(); return; }
633
- if (!res.ok) return; // module off / route absent → the section stays hidden
634
- renderCrossProject(res.data);
635
- renderPrivacy(res.data);
636
- section.hidden = false;
637
- const privacy = $('#privacy-scroll');
638
- if (privacy) privacy.hidden = false;
639
- if (!crossProjectWired) {
640
- crossProjectWired = true;
641
- $('#cross-project-rows').addEventListener('click', (e) => {
642
- const b = e.target.closest('.sound-toggle');
643
- if (!b || b.disabled) return;
644
- saveCrossProject(b.dataset.on === '1', b); // shown → hide; hidden → show
645
- });
646
- $('#privacy-rows').addEventListener('click', (e) => {
647
- const b = e.target.closest('.sound-toggle');
648
- if (!b || b.disabled) return;
649
- const row = b.closest('[data-privacy]');
650
- const on = b.dataset.on === '1';
651
- const body = row && row.dataset.privacy === 'visibility'
652
- ? { account_visibility: on ? 'private' : 'public' }
653
- : { recruiter_discoverable: !on };
654
- savePrivacy(body, '#privacy-status', privacyMsg(row && row.dataset.privacy, !on), b);
655
- });
656
- }
657
- } catch (err) {
658
- // Network/parse failure on an optional module section — stay hidden, log only.
659
- console.error('[settings] failed to load cross-project', err);
660
- }
490
+ // Handle, bio, links, public/private, recruiters and the hide-my-numbers switch
491
+ // belong to the PLATFORM account, not to this project (ADR 0337 D4.3), so they
492
+ // are edited once, on the hub's account page — this hall used to carry the
493
+ // privacy half as its own two sections, and now carries one row that points
494
+ // there (the /my-projects precedent: a user-level page that lives on the hub
495
+ // ships no copy in the hall). The hub's origin comes from OTB.hubOrigin()
496
+ // (dom-utils.js): a bare https origin, or null on a vanilla instance, which
497
+ // has no hub and so no row.
498
+ function loadAccountHub() {
499
+ const section = $('#account-hub-scroll');
500
+ const link = $('#account-hub-link');
501
+ const origin = window.OTB.hubOrigin();
502
+ if (!section || !link || !origin) return; // no hub → the row stays hidden
503
+ link.href = `${origin}/account`;
504
+ section.hidden = false;
661
505
  }
662
506
 
663
507
  // ---- section 2: CRAFT (discipline preferences) ---------------------------
@@ -1215,8 +1059,11 @@
1215
1059
  const PANEL_ANCHORS = {
1216
1060
  'connections': 'connections-scroll',
1217
1061
  'display-name': 'display-name-scroll',
1218
- 'cross-project': 'cross-project-scroll',
1219
- 'privacy': 'privacy-scroll',
1062
+ // task 1004259: the two privacy panels became one link-out row; their old
1063
+ // anchors still land on it (the hall profile links /settings#cross-project).
1064
+ 'profile-privacy': 'account-hub-scroll',
1065
+ 'cross-project': 'account-hub-scroll',
1066
+ 'privacy': 'account-hub-scroll',
1220
1067
  'disciplines': 'craft-scroll',
1221
1068
  'cli': 'cli-scroll',
1222
1069
  'art-key': 'art-key',
@@ -1363,9 +1210,9 @@
1363
1210
  // (Rendering — the card-richness ladder — is settings-render.js, on the same
1364
1211
  // precedent: it became a four-rung picker in task 1002518 and this file is
1365
1212
  // against the 1500-line ratchet.)
1366
- // Cross-project record (hide flag) — reveals only where the platform-identity
1367
- // module answers (the hub); silent no-op elsewhere (task 1002279).
1368
- await loadCrossProject();
1213
+ // Profile and privacy — one row linking out to the hub's account page
1214
+ // (task 1004259); hidden where the page carries no hub origin.
1215
+ loadAccountHub();
1369
1216
  // Image key (per-builder Gemini key, task 1013) — own try/catch; `me` carries
1370
1217
  // the `needs` art_key item that drives the covered/action-needed banner.
1371
1218
  await loadArtKey(me);
@@ -7,7 +7,7 @@
7
7
  "ignoreRequests": ["/api/(gds|bongos)/"],
8
8
  "ignoreConsole": ["/api/(gds|bongos)/", "\\b401\\b", "\\b404\\b", "Unauthorized", "Not Found", "load failed", "Password field"],
9
9
  "states": {
10
- "account": { "auth": true, "actions": [["wait", 900]], "expect": { "visible": ["#settings-h1", "#display-name-input", "#craft-chips", "#privacy-rows"] } },
10
+ "account": { "auth": true, "actions": [["wait", 900]], "expect": { "visible": ["#settings-h1", "#display-name-input", "#craft-chips"] } },
11
11
  "access": { "auth": true, "url": "/settings#access", "actions": [["wait", 900]], "expect": { "visible": ["#cli-reissue-btn", "#sessions-list", "#artkey-card"] } },
12
12
  "preferences": { "auth": true, "url": "/settings#preferences", "actions": [["wait", 900]], "expect": { "visible": ["#wander-rows", "#render-rows"] } },
13
13
  "sound": { "auth": true, "url": "/settings#sound", "actions": [["wait", 900]], "expect": { "visible": ["#voices-table", "#sound-rows", "#event-sound-rows"] } },
@@ -334,7 +334,7 @@
334
334
  // dropped where the module is off — and since task 1004296 it STAYS dropped: /deploy
335
335
  // is the platform hall's page alone, and every other hall takes its update from
336
336
  // Settings → Software update, whose Update button carries the hub hand-off.
337
- { id: 'deploy', label: 'Deploy', icon: 'admin', href: '/deploy', gate: 'archon', module: 'provisioning' },
337
+ { id: 'deploy', label: 'Deploy', icon: 'admin', href: '/deploy', gate: 'archon', module: 'provisioning render-deploy' },
338
338
  // task 1001949: what each hosted project costs. 'government' mirrors the metic floor
339
339
  // of `provisioning.fleet.manage`, the atom its page and its one read check (the
340
340
  // Deploy precedent above); dropped where the provisioning module is not enabled.
@@ -918,7 +918,9 @@
918
918
  }
919
919
  document.querySelectorAll('#app-sidebar [data-gate]').forEach(function (el) {
920
920
  var ok = gateAllows(el.dataset.gate);
921
- if (ok && el.dataset.module && !moduleOn(el.dataset.module)) ok = false;
921
+ // A space-separated list is any-of: the Deploy item is the provisioning door's on the
922
+ // platform hall and the render-deploy module's on a Render project's hall (task 1004270).
923
+ if (ok && el.dataset.module && !el.dataset.module.split(' ').some(moduleOn)) ok = false;
922
924
  el.hidden = !ok;
923
925
  });
924
926
  // AFTER the gate loop: applyCounts reads el.hidden, which the loop above is
@@ -109,12 +109,17 @@ html[data-fullscreen="on"] body[data-page="studio"] .fullscreen-toggle {
109
109
  inset: 0;
110
110
  pointer-events: none;
111
111
  z-index: 0;
112
+ /* On a screen taller than the ~900px artboard the whole scene drops by half
113
+ the extra height, so it stays centred instead of sitting in the top 700px
114
+ over a strip of bare floor (task 1004344). Zero at or below the artboard, so
115
+ the design of record is unchanged there. % is of the room's own height. */
116
+ --room-drop: max(0px, calc((100% - 900px) / 2));
112
117
  }
113
118
  .st-room > *,
114
119
  .st-window > * { position: absolute; }
115
120
  .st-window {
116
121
  left: max(calc(50% + 60px), 680px);
117
- top: 86px;
122
+ top: calc(86px + var(--room-drop));
118
123
  width: 560px;
119
124
  height: 560px;
120
125
  box-sizing: border-box;
@@ -151,22 +156,22 @@ html[data-fullscreen="on"] body[data-page="studio"] .fullscreen-toggle {
151
156
  .st-light--7 { left: 250px; bottom: 120px; opacity: .5; }
152
157
  .st-mullion { left: 264px; top: 0; width: 0; height: 100%; border-left: 12px solid var(--st-frame); }
153
158
  .st-floor {
154
- left: 0; right: 0; top: 700px; bottom: 0;
159
+ left: 0; right: 0; top: calc(700px + var(--room-drop)); bottom: 0;
155
160
  min-height: 200px;
156
161
  background: var(--st-floor);
157
162
  border-top: 1px solid var(--st-floor-edge);
158
163
  }
159
164
  .st-glow {
160
- left: calc(50% + 440px); top: 470px; width: 300px; height: 300px;
165
+ left: calc(50% + 440px); top: calc(470px + var(--room-drop)); width: 300px; height: 300px;
161
166
  border-radius: 50%;
162
167
  background: radial-gradient(circle, var(--st-glow) 0%, var(--st-glow-out) 65%);
163
168
  }
164
- .st-lamp-stem { left: calc(50% + 570px); top: 560px; width: 6px; height: 140px; background: var(--st-stem); }
165
- .st-lamp-shade { left: calc(50% + 526px); top: 540px; width: 94px; height: 46px; border-radius: 48px 48px 6px 6px; background: var(--st-shade); }
166
- .st-mug { left: calc(50% + 340px); top: 650px; width: 46px; height: 52px; border-radius: 0 0 10px 10px; background: var(--st-mug); }
169
+ .st-lamp-stem { left: calc(50% + 570px); top: calc(560px + var(--room-drop)); width: 6px; height: 140px; background: var(--st-stem); }
170
+ .st-lamp-shade { left: calc(50% + 526px); top: calc(540px + var(--room-drop)); width: 94px; height: 46px; border-radius: 48px 48px 6px 6px; background: var(--st-shade); }
171
+ .st-mug { left: calc(50% + 340px); top: calc(650px + var(--room-drop)); width: 46px; height: 52px; border-radius: 0 0 10px 10px; background: var(--st-mug); }
167
172
  .st-leaf { border-radius: 50% 50% 0 0; }
168
- .st-leaf--1 { left: calc(50% + 180px); top: 610px; width: 64px; height: 90px; background: var(--st-leaf-1); }
169
- .st-leaf--2 { left: calc(50% + 220px); top: 590px; width: 50px; height: 110px; background: var(--st-leaf-2); }
173
+ .st-leaf--1 { left: calc(50% + 180px); top: calc(610px + var(--room-drop)); width: 64px; height: 90px; background: var(--st-leaf-1); }
174
+ .st-leaf--2 { left: calc(50% + 220px); top: calc(590px + var(--room-drop)); width: 50px; height: 110px; background: var(--st-leaf-2); }
170
175
 
171
176
  /* Everything the artist reads or presses sits above the room. */
172
177
  .st-top,
@@ -503,6 +508,7 @@ html.st-open { overflow: hidden; }
503
508
  --------------------------------------------------------------------------- */
504
509
  @media (max-width: 760px) {
505
510
  .studio { padding: 20px 16px 140px; }
511
+ .st-room { --room-drop: 0px; }
506
512
  .st-top { gap: 12px; }
507
513
  .st-tally {
508
514
  margin-left: 0;
@@ -14,7 +14,7 @@
14
14
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
15
15
  <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
16
16
  <link rel="stylesheet" href="/builders/studio.css?v=2026-09-28-tw16">
17
- <link rel="stylesheet" href="/builders/approval-queue.css">
17
+ <link rel="stylesheet" href="/builders/approval-queue.css?v=2026-09-29-aqfix">
18
18
  </head>
19
19
  <!-- THE STUDIO HOME (task 1003971 / BV2.TW16, goal 1000095; ADR 0341 D2, D5,
20
20
  D8 and D11). docs/design/mocks/tweak-mode/Main.dc.html is the master design:
@@ -176,7 +176,7 @@
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.js?v=2026-09-28-archive"></script>
179
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
180
180
  <script src="/builders/palette.js" defer></script>
181
181
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
182
182
  <!-- The kit, for its ONE renderer: the full-size ship-time visual figure
@@ -185,8 +185,9 @@
185
185
  <script src="/builders/hall-kit.js"></script>
186
186
  <!-- The Approval queue panel (task 1004323): its pure rules, then the panel.
187
187
  studio.js mounts it with its own WRITE_ROUTES. -->
188
- <script src="/builders/approval-queue-lib.js"></script>
189
- <script src="/builders/approval-queue.js"></script>
188
+ <script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
189
+ <script src="/builders/approval-queue-lib.js?v=2026-09-29-aqfix"></script>
190
+ <script src="/builders/approval-queue.js?v=2026-09-29-aqfix"></script>
190
191
  <!-- The page names and the draft body (the tweak editor's pure rules), then
191
192
  the studio's own. -->
192
193
  <script src="/builders/tweak-editor-lib.js?v=2026-09-28-tw23"></script>
@@ -57,7 +57,7 @@
57
57
 
58
58
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
59
59
  time so the chrome exists before the deferred page scripts execute. -->
60
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
60
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
61
61
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
62
62
  <script src="/builders/palette.js" defer></script>
63
63
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
@@ -30,8 +30,11 @@ body[data-page="goal-map"], body[data-page="thinking"] .sky-stage {
30
30
  --face: Manrope, "Segoe UI", system-ui, -apple-system, sans-serif;
31
31
  }
32
32
  html, body { height: 100%; }
33
- body[data-page="goal-map"], body[data-page="thinking"] .sky-stage { margin: 0; background: var(--void); color: var(--ink); font-family: var(--face); font-size: 15px; line-height: 1.5; overflow: hidden; -webkit-font-smoothing: antialiased; }
34
- body[data-page="thinking"] { overflow: hidden; }
33
+ body[data-page="goal-map"], body[data-page="thinking"] .sky-stage { margin: 0; background: var(--void); color: var(--ink); font-family: var(--face); font-size: 15px; line-height: 1.5; overflow: hidden; overflow: clip; -webkit-font-smoothing: antialiased; }
34
+ /* clip, not just hidden (task 1004342): a hidden-overflow box is still scrollable, so
35
+ focus scrolled the stage sideways to show the footer hint and the whole sky slid
36
+ left. clip cuts the same pixels and cannot be scrolled; hidden stays as the fallback. */
37
+ body[data-page="thinking"] { overflow: hidden; overflow: clip; }
35
38
  :where(body[data-page="goal-map"], .sky-stage) button { font-family: inherit; }
36
39
  :where(body[data-page="goal-map"], .sky-stage) a { color: var(--accent); }
37
40
  :where(body[data-page="goal-map"], .sky-stage) .lbl { font: var(--label); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); }
@@ -196,7 +199,7 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 64px; }
196
199
  .obj { display: inline-flex; align-items: center; gap: 9px; border: 0; background: transparent; color: var(--ink-faint); font: var(--label); letter-spacing: .14em; text-transform: uppercase; cursor: pointer; padding: 6px 0; }
197
200
  .obj:hover { color: var(--ink); }
198
201
  .obj .g { width: 20px; height: 18px; display: grid; place-items: center; }
199
- .alt { margin-left: auto; text-align: right; color: var(--ink-dim); font: var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
202
+ .alt { margin-left: auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; text-align: right; color: var(--ink-dim); font: var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
200
203
  .alt b { color: var(--ink-soft); font-weight: 400; }
201
204
  .hint { color: var(--ink-ghost); }
202
205
  .time { display: flex; align-items: center; gap: 8px; width: min(360px, 100%); align-self: center; padding: 3px 4px 3px 4px; border-radius: 999px; background: rgba(0,0,0,.45); box-shadow: inset 0 0 0 1px var(--rule-soft); opacity: .8; }
@@ -138,7 +138,7 @@
138
138
  </div>
139
139
  </div>
140
140
 
141
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
141
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
142
142
  <script src="/builders/palette.js" defer></script>
143
143
 
144
144
  <script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
@@ -148,7 +148,88 @@
148
148
  return PAGE_ID_RE.test(id) ? id : null;
149
149
  }
150
150
 
151
- const api = { NOTES, sectionsOf, isChanged, noteFor, draftBody, changedCount, counterText, initialValues, rowsFor, liveUrl, uploadedLabel, uploadRefusal, displayTitle, pageIdFrom };
151
+ // THE CHECK (task 1004385). A line the artist has read and is happy with,
152
+ // rewritten or not, carries a green check. It is the artist's own place-keeper
153
+ // and never leaves the browser: the server has no such field. Shift+Enter is
154
+ // the fast way through a page: check this line, land in the next.
155
+ const HINT = 'Shift + Enter checks a line and jumps to the next one. It is the quickest way through a page.';
156
+
157
+ function nextLineKey(lines, key) {
158
+ const list = lines || [];
159
+ const i = list.findIndex((l) => l.key === key);
160
+ return i >= 0 && i + 1 < list.length ? list[i + 1].key : null;
161
+ }
162
+
163
+ // Only lines still on the page count, so a reading that moved cannot inflate it.
164
+ function confirmedCount(lines, confirmed) {
165
+ let n = 0;
166
+ for (const l of lines || []) if (confirmed && confirmed.has(l.key)) n += 1;
167
+ return n;
168
+ }
169
+
170
+ function hintText(checked, total) {
171
+ return `${HINT} ${checked} of ${total} checked.`;
172
+ }
173
+
174
+ // Remembered per page AND reading: a page that changed under the artist has
175
+ // new lines, and a stale check on L0007 would tick the wrong words.
176
+ function confirmStoreKey(pageId, readingHash) {
177
+ return `hall-tweak-checked:${pageId}:${readingHash}`;
178
+ }
179
+
180
+ function parseConfirmed(raw, lines) {
181
+ const known = new Set((lines || []).map((l) => l.key));
182
+ let arr = [];
183
+ try { arr = JSON.parse(raw || '[]'); } catch (_) { arr = []; }
184
+ return new Set(Array.isArray(arr) ? arr.filter((k) => known.has(k)) : []);
185
+ }
186
+
187
+ // SPLIT VIEW (task 1004386). The live site answers X-Frame-Options: SAMEORIGIN
188
+ // (set at the proxy in front of the app, not in this repo), so a page on
189
+ // another host than the editor's cannot be framed: a hall page can, a landing
190
+ // or status page cannot. For those the split view is a second window docked to
191
+ // the bottom half of the screen.
192
+ function canFrame(url, loc) {
193
+ try { return new URL(url, loc.href || loc.origin).origin === (loc.origin || new URL(loc.href).origin); } catch (_) { return false; }
194
+ }
195
+ function splitWindowFeatures(scr) {
196
+ const s = scr || {};
197
+ const w = s.availWidth || 1280;
198
+ const h = s.availHeight || 800;
199
+ const left = s.availLeft || 0;
200
+ const top = (s.availTop || 0) + Math.floor(h / 2);
201
+ return `popup=yes,width=${w},height=${Math.floor(h / 2)},left=${left},top=${top}`;
202
+ }
203
+
204
+ // THE SPLIT BUTTON AND THE DIVIDER (task 1004386, owner-amended). "open the
205
+ // page" repeats the artist's last choice; the arrow beside it offers both. The
206
+ // split's size is the share of the window the live page takes, kept between
207
+ // 20% and 80% so neither half can vanish.
208
+ const OPEN_WORDS = Object.freeze({ tab: 'open the page ↗', split: 'open in split view' });
209
+ function openChoice(raw) { return raw === 'split' ? 'split' : 'tab'; }
210
+ const SPLIT_MIN = 0.2;
211
+ const SPLIT_MAX = 0.8;
212
+ const SPLIT_STEP = 0.05;
213
+ function clampSplit(f) {
214
+ const n = Number(f);
215
+ if (f === null || f === undefined || f === '' || !Number.isFinite(n)) return 0.5;
216
+ return Math.min(SPLIT_MAX, Math.max(SPLIT_MIN, Math.round(n * 100) / 100));
217
+ }
218
+ // The divider sits on the frame's top edge, so the page's share is what lies below the pointer.
219
+ function splitFromPointer(clientY, innerHeight) {
220
+ if (!(innerHeight > 0)) return 0.5;
221
+ return clampSplit((innerHeight - clientY) / innerHeight);
222
+ }
223
+ // Up grows the live page, down shrinks it: the divider moves with the arrow.
224
+ function splitFromKey(current, key) {
225
+ if (key === 'ArrowUp') return clampSplit(clampSplit(current) + SPLIT_STEP);
226
+ if (key === 'ArrowDown') return clampSplit(clampSplit(current) - SPLIT_STEP);
227
+ if (key === 'Home') return SPLIT_MIN;
228
+ if (key === 'End') return SPLIT_MAX;
229
+ return null;
230
+ }
231
+
232
+ const api = { OPEN_WORDS, openChoice, clampSplit, splitFromPointer, splitFromKey, canFrame, splitWindowFeatures, HINT, nextLineKey, confirmedCount, hintText, confirmStoreKey, parseConfirmed, NOTES, sectionsOf, isChanged, noteFor, draftBody, changedCount, counterText, initialValues, rowsFor, liveUrl, uploadedLabel, uploadRefusal, displayTitle, pageIdFrom };
152
233
  if (typeof window !== 'undefined') window.OTBTweakEditor = api;
153
234
  if (typeof module !== 'undefined' && module.exports) module.exports = api;
154
235
  })();