@bongos/core 1.19.1074 → 1.19.1076

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 (179) hide show
  1. package/.bongos-core.json +196 -211
  2. package/.claude/skills/blocker-solve/SKILL.md +1 -1
  3. package/README.md +1 -1
  4. package/bin/bongos.js +1 -3
  5. package/docs/adr/0031-cloud-dev-environments-for-builders.md +1 -1
  6. package/docs/adr/0035-builder-onboarding-three-paths.md +1 -1
  7. package/docs/adr/0044-per-box-live-game-preview.md +1 -1
  8. package/docs/adr/0045-devbox-desktop-app.md +1 -1
  9. package/docs/adr/0046-sandbox-first-review-gate.md +1 -1
  10. package/docs/adr/0052-sandbox-for-everyone-game-only-preview.md +1 -1
  11. package/docs/adr/0053-scoped-dev-box-session.md +1 -1
  12. package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
  13. package/docs/adr/0057-container-cost-ledger.md +1 -1
  14. package/docs/adr/0059-single-approval-remove-devbox-approval-gate.md +1 -1
  15. package/docs/adr/0071-box-confirm-before-destroyed-and-drift-reconcile.md +1 -1
  16. package/docs/adr/0072-bongos-app-mac-signed-first-windows-deferred.md +1 -1
  17. package/docs/adr/0072-dev-box-code-staleness-visibility.md +1 -1
  18. package/docs/adr/0104-trust-gds-api-channel-in-auto-mode.md +1 -1
  19. package/docs/adr/0123-box-idle-sweep-autosave-before-destroy.md +1 -1
  20. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
  21. package/docs/adr/0145-devbox-app-branding-driven-module.md +1 -1
  22. package/docs/adr/0148-task-scoped-box-source-access.md +2 -2
  23. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +1 -1
  24. package/docs/adr/0193-pause-task-scoped-box-slices.md +1 -1
  25. package/docs/adr/0277-a-box-is-in-use-only-while-a-human-is-attached.md +1 -1
  26. package/docs/adr/0346-dev-boxes-are-retired.md +69 -0
  27. package/docs/adr/README.md +1 -0
  28. package/docs/api/openapi.json +4 -4
  29. package/docs/architecture.md +5 -6
  30. package/docs/branding-contract.md +2 -2
  31. package/docs/canonical-permissions.md +10 -18
  32. package/docs/copy-inventory.md +3 -3
  33. package/docs/copy-registry.json +3 -3
  34. package/docs/design/gate-navigation-direction.md +1 -1
  35. package/docs/design/hall-direction-v2.md +3 -3
  36. package/docs/design/modular-architecture/00-research-report.md +1 -1
  37. package/docs/design/modular-architecture/01-architecture.md +3 -3
  38. package/docs/design/modular-architecture/02-module-map.md +1 -2
  39. package/docs/design/modular-architecture/03-builder-flows.md +22 -20
  40. package/docs/design/modular-architecture/04-module-lifecycle.md +1 -1
  41. package/docs/design/modular-architecture/README.md +4 -2
  42. package/docs/design/reviews/hall-v2/README.md +2 -2
  43. package/docs/design/vanilla-hall-ui-redesign-scope.md +4 -4
  44. package/docs/file-map.md +5 -5
  45. package/docs/handoff-template.md +1 -1
  46. package/docs/module-api-changelog.md +8 -0
  47. package/docs/modules-contract.md +11 -14
  48. package/docs/page-readings.json +63 -63
  49. package/docs/recipes/bongos-cli-release.md +1 -2
  50. package/docs/recipes/multi-builder-merge.md +1 -1
  51. package/docs/recipes/ops-gotchas.md +0 -9
  52. package/docs/recipes/self-host.md +1 -1
  53. package/docs/recipes/windows-builders.md +1 -1
  54. package/migrations/core_258_drop_dev_box_tables.sql +3 -0
  55. package/modules/copy-desk/routes/copy-desk.js +1 -1
  56. package/modules/discord/ship-broadcast.js +3 -3
  57. package/modules/economy/credits.js +2 -2
  58. package/modules/hall-ui/public/palette.js +143 -3
  59. package/modules/ideas/projection.js +1 -1
  60. package/modules/lifecycle/db-ship.js +4 -4
  61. package/modules/lifecycle/github-push.js +12 -14
  62. package/modules/lifecycle/publish-reconciler.js +10 -10
  63. package/modules/lifecycle/routes/tasks.js +8 -8
  64. package/modules/lifecycle/ship-card.js +1 -1
  65. package/modules/lifecycle/task-visuals.js +2 -2
  66. package/modules/memory/routes/memory.js +2 -2
  67. package/modules/platform-identity/routes/sso.js +3 -2
  68. package/modules/provisioning/capacity.js +1 -1
  69. package/modules/provisioning/provisioning.js +4 -3
  70. package/modules/provisioning/routes/provisioning.js +4 -4
  71. package/modules/sessions/db.js +1 -1
  72. package/package-lock.json +2 -2
  73. package/package.json +1 -1
  74. package/release-notes.json +28 -0
  75. package/scripts/gds/artifact-staleness.js +2 -2
  76. package/scripts/gds/backfill-task-rewards.js +5 -5
  77. package/scripts/gds/claude-materialize.js +2 -2
  78. package/scripts/gds/cli-lib.js +7 -7
  79. package/scripts/gds/codemod-rename-src-gds.js +1 -1
  80. package/scripts/gds/context-pack.js +5 -7
  81. package/scripts/gds/dev-box-guard.js +165 -0
  82. package/scripts/gds/doctor.js +3 -3
  83. package/scripts/gds/fitness-checks-packaging.js +4 -4
  84. package/scripts/gds/fitness-lib.js +10 -0
  85. package/scripts/gds/fitness-ratchets.js +1 -1
  86. package/scripts/gds/fitness.js +2 -10
  87. package/scripts/gds/gen-api-client.js +4 -4
  88. package/scripts/gds/gen-api-docs.js +1 -1
  89. package/scripts/gds/gen-repo-map.js +13 -14
  90. package/scripts/gds/gen-session-index.js +8 -8
  91. package/scripts/gds/leak-scan-allowlist.js +1 -1
  92. package/scripts/gds/plain-cards.js +1 -1
  93. package/scripts/gds/push-path-brief.js +10 -10
  94. package/scripts/gds/rename-history-check.js +1 -1
  95. package/scripts/gds/run-unit-tests.js +4 -0
  96. package/scripts/gds/session-digest-build.js +2 -2
  97. package/scripts/gds/ship-deploy-target.js +13 -13
  98. package/scripts/gds/ship-flow.js +1 -1
  99. package/scripts/gds/ship-land.js +6 -6
  100. package/scripts/gds/ship-merge.js +4 -4
  101. package/scripts/gds/ship-preflight-steps.js +4 -4
  102. package/scripts/gds/ship-regen.js +9 -12
  103. package/scripts/gds/ship.js +9 -9
  104. package/scripts/gds/skill-preflight.js +9 -8
  105. package/src/bongos/api-prefix.js +4 -4
  106. package/src/bongos/module-scope-map.js +1 -1
  107. package/src/bongos/platform-visibility-gate.js +1 -1
  108. package/src/bongos/routes/me.js +1 -1
  109. package/src/bongos/routes/security.js +3 -3
  110. package/src/bongos/routes.js +2 -2
  111. package/src/bongos/serve-internal.js +5 -6
  112. package/src/bongos-downloads.js +1 -6
  113. package/src/branding.js +2 -2
  114. package/src/build-info.js +4 -6
  115. package/src/instance-config.js +2 -2
  116. package/src/module-api.js +3 -3
  117. package/src/module-loader/manifest-schema.js +1 -1
  118. package/tests/api_alias_caller_ratchet.mjs +1 -1
  119. package/tests/api_alias_exceptions.mjs +1 -1
  120. package/tests/api_client.mjs +1 -1
  121. package/tests/api_path_404.mjs +1 -1
  122. package/tests/backfill_task_rewards.mjs +1 -1
  123. package/tests/{box_ship_permissions.mjs → cli_allowlist_permissions.mjs} +5 -5
  124. package/tests/cli_sessions.mjs +1 -1
  125. package/tests/context_pack.mjs +13 -13
  126. package/tests/copy_inventory.mjs +7 -7
  127. package/tests/core_258_box_session_claims_db.mjs +180 -0
  128. package/tests/credit_grant.mjs +2 -2
  129. package/tests/criterion_suggest.mjs +1 -1
  130. package/tests/design_tokens_sync.mjs +3 -2
  131. package/tests/fitness.mjs +89 -0
  132. package/tests/fitness_ratchets.mjs +2 -2
  133. package/tests/github_push_land_proof.mjs +1 -1
  134. package/tests/goal_advisory.mjs +3 -3
  135. package/tests/goal_suggest.mjs +3 -3
  136. package/tests/government_abuse_matrix.mjs +4 -3
  137. package/tests/hall_audit.mjs +1 -1
  138. package/tests/hall_palette.mjs +224 -11
  139. package/tests/helpers.mjs +1 -1
  140. package/tests/host_topology_skips.mjs +2 -2
  141. package/tests/migration_namespace.mjs +6 -6
  142. package/tests/module_api.mjs +2 -2
  143. package/tests/module_cli.mjs +5 -5
  144. package/tests/module_manifest.mjs +13 -13
  145. package/tests/module_route_rank.mjs +2 -2
  146. package/tests/modules_route_wiring.mjs +1 -1
  147. package/tests/onboarding_route_signals.mjs +1 -1
  148. package/tests/paste_token_session_store.mjs +1 -1
  149. package/tests/platform_visibility_gate.mjs +1 -1
  150. package/tests/project_modules_ui.mjs +17 -17
  151. package/tests/projects_hub_module_picker.mjs +75 -74
  152. package/tests/provisioning_capacity.mjs +1 -1
  153. package/tests/provisioning_recommendations.mjs +3 -2
  154. package/tests/publish_branch_route.mjs +1 -1
  155. package/tests/publish_manifest.mjs +1 -1
  156. package/tests/publish_reconciler.mjs +8 -8
  157. package/tests/push_path_brief.mjs +10 -9
  158. package/tests/regrade_eligibility.mjs +1 -1
  159. package/tests/rename_history_restraint.mjs +1 -1
  160. package/tests/repo_map.mjs +9 -9
  161. package/tests/seam_wiring_guard.mjs +11 -11
  162. package/tests/search_isolation.mjs +3 -3
  163. package/tests/session_records.mjs +1 -1
  164. package/tests/session_rename_fallback.mjs +1 -1
  165. package/tests/session_start_freshness.mjs +3 -3
  166. package/tests/session_token_hash_db.mjs +1 -1
  167. package/tests/ship_card.mjs +1 -1
  168. package/tests/ship_ci_deploy.mjs +17 -17
  169. package/tests/ship_premerge.mjs +1 -1
  170. package/tests/ship_resume.mjs +1 -1
  171. package/tests/skill_preflight.mjs +2 -2
  172. package/tests/skip_is_not_pass.mjs +1 -1
  173. package/tests/terms_acceptance.mjs +1 -1
  174. package/docs/design/reviews/hall-v2/harbor--archon.webp +0 -0
  175. package/docs/design/reviews/hall-v2/harbor--metic.webp +0 -0
  176. package/docs/design/reviews/hall-v2/harbor--xenos.webp +0 -0
  177. package/docs/design/reviews/hall-v2/pair--archon.webp +0 -0
  178. package/docs/design/reviews/hall-v2/pair--metic.webp +0 -0
  179. package/docs/design/reviews/hall-v2/pair--xenos.webp +0 -0
@@ -72,16 +72,16 @@ await test("kind='bug' with no goal at all is quiet too — a fix needs no home
72
72
  });
73
73
 
74
74
  await test('a REAL goal is silent — null, not an empty object', () => {
75
- assert.equal(adviseGoal({ versionId: V, goalId: 1000025, goalTitle: 'Builder experience — hall, dev box, and skills' }), null);
75
+ assert.equal(adviseGoal({ versionId: V, goalId: 1000025, goalTitle: 'Builder experience — hall, onboarding, and skills' }), null);
76
76
  });
77
77
 
78
78
  await test("kind='bug' under a real goal is silent too — already silent, kind changes nothing", () => {
79
- assert.equal(adviseGoal({ versionId: V, goalId: 1000025, goalTitle: 'Builder experience — hall, dev box, and skills', kind: 'bug' }), null);
79
+ assert.equal(adviseGoal({ versionId: V, goalId: 1000025, goalTitle: 'Builder experience — hall, onboarding, and skills', kind: 'bug' }), null);
80
80
  });
81
81
 
82
82
  // ---- adviseGoal: the third reason — no_criterion_link (task 1003157) ----
83
83
 
84
- const REAL_GOAL_TITLE = 'Builder experience — hall, dev box, and skills';
84
+ const REAL_GOAL_TITLE = 'Builder experience — hall, onboarding, and skills';
85
85
  const SOME_CRITERIA = [
86
86
  { id: 11, criterion_md: 'Every onboarding page renders under 2s' },
87
87
  { id: 12, criterion_md: 'The hall shows a builder their own karma trend' },
@@ -30,7 +30,7 @@ const { test, summary } = makeRunner();
30
30
  const V = 'BONGOS-V1';
31
31
  const GOALS = [
32
32
  { id: 1000024, title: 'Build-pipeline integrity', scope_modules: ['lifecycle', 'grading'] },
33
- { id: 1000025, title: 'Builder experience — hall, dev box, and skills', scope_modules: ['hall-ui', 'dev-box'] },
33
+ { id: 1000025, title: 'Builder experience — hall, tool box, and skills', scope_modules: ['hall-ui', 'specialities'] },
34
34
  { id: 1000071, title: 'Idea routing — work that has a home lands in it', scope_modules: ['ideas', 'discord'] },
35
35
  { id: 1000003, title: `${V} — general`, scope_modules: [] },
36
36
  ];
@@ -76,8 +76,8 @@ await test('RANKS: ties break by ascending id so a picker renders stably', () =>
76
76
 
77
77
  await test('LIMIT (known + accepted): token overlap can match on an incidental word', () => {
78
78
  // Real example found while building this: "the goals page search BOX is broken"
79
- // scores the DEV BOX goal, because both contain the token 'box'. Bag-of-words
80
- // matching cannot tell "search box" from "dev box".
79
+ // scores the "tool box" goal, because both contain the token 'box'. Bag-of-words
80
+ // matching cannot tell "search box" from "tool box".
81
81
  //
82
82
  // This is NOT hidden — it is why every suggestion carries `score` and
83
83
  // `matched_terms`: a caller can show WHY it matched and a human decides. A
@@ -365,9 +365,10 @@ test('V8: a forced principal grant resolves to nothing (structurally dropped)',
365
365
  });
366
366
 
367
367
  test('V9: an undeclared session source is refused outright, never treated as "full"', () => {
368
- // Rows with source 'box' stay in builder_sessions until task 1003895 deletes them.
369
- // They must be undeclared to BOTH loaders, which is what makes requireBuilder and
370
- // the page gate refuse them (tests/government_session_scope.mjs runs the gate).
368
+ // core_258 (task 1003895) deleted the rows with source 'box' and narrowed the source
369
+ // CHECK to cli/web. 'box' must still be undeclared to BOTH loaders, so a stray row of
370
+ // that source is refused by requireBuilder and the page gate rather than honoured
371
+ // (tests/government_session_scope.mjs runs the gate).
371
372
  for (const source of ['box', 'not-a-source']) {
372
373
  assert.equal(sessionScopes.scopeFor(source), null, `"${source}" must not be a declared session source`);
373
374
  }
@@ -464,7 +464,7 @@ test('the colour-literal guard CATCHES a reintroduction, and tolerates a task re
464
464
  assert.ok('a { color: #a04040; }'.match(re), 'a bare hex in a colour value is caught');
465
465
  assert.ok('x = \'<span style="color:var(--olive,#5b7a4d)">\''.match(colourLiteralRe()),
466
466
  'a hex FALLBACK behind a var() is caught — that is how --olive hid');
467
- assert.equal('// #807: the dev-box panel'.match(colourLiteralRe()), null,
467
+ assert.equal('// #807: the ship panel'.match(colourLiteralRe()), null,
468
468
  'a task reference in a comment is not a colour');
469
469
  assert.equal('a { color: var(--ok); }'.match(colourLiteralRe()), null,
470
470
  'a token is exactly what this rule wants');
@@ -28,21 +28,41 @@ const SHELL_SRC = read('shell.js');
28
28
 
29
29
  // Boot palette.js against a DOM stub whose sidebar carries `navItems`, and whose
30
30
  // `visibleIds` decide which section groups are on offer. Returns window.OTBPalette.
31
- function bootPalette({ navItems = [], visibleIds = ['nav-standing', 'nav-work', 'shell-user'] } = {}) {
31
+ //
32
+ // `registry` memoizes querySelector by selector string (keyed globally within one
33
+ // boot, same as a real single-instance-id contract) so `build()`'s one-time
34
+ // `root.querySelector('#palette-input')` / `'#palette-list'` calls hand back the
35
+ // SAME stub every time — real enough to fire DOM events at and read innerHTML
36
+ // back from, which the pure-compute-only harness never needed before task 1003460
37
+ // wired a real fetch() into the input handler.
38
+ function bootPalette({ navItems = [], visibleIds = ['nav-standing', 'nav-work', 'shell-user'], modules = null, fetchImpl = undefined } = {}) {
39
+ const registry = {};
32
40
  const makeNav = (it) => ({
33
41
  hidden: !!it.hidden,
34
42
  target: it.external ? '_blank' : '',
35
43
  getAttribute: (n) => (n === 'href' ? it.href : null),
36
44
  querySelector: (sel) => (sel === '.nav-item__label' ? { textContent: it.label } : null),
37
45
  });
38
- const stubEl = () => ({
39
- hidden: false, dataset: {}, textContent: '', value: '', className: '',
40
- classList: { add() {}, remove() {}, contains() { return false; }, toggle() { return false; } },
41
- setAttribute() {}, getAttribute() { return null; },
42
- addEventListener() {}, appendChild() {}, focus() {}, querySelector() { return stubEl(); },
43
- querySelectorAll() { return []; },
44
- set innerHTML(_v) {}, get innerHTML() { return ''; },
45
- });
46
+ const stubEl = (sel) => {
47
+ const listeners = {};
48
+ const el = {
49
+ hidden: false, dataset: {}, textContent: '', value: '', className: '',
50
+ classList: { add() {}, remove() {}, contains() { return false; }, toggle() { return false; } },
51
+ setAttribute() {}, getAttribute() { return null; },
52
+ addEventListener(type, fn) { (listeners[type] = listeners[type] || []).push(fn); },
53
+ fire(type, ev) { (listeners[type] || []).forEach((fn) => fn(ev || {})); },
54
+ appendChild() {}, focus() {},
55
+ querySelector(childSel) {
56
+ if (!registry[childSel]) registry[childSel] = stubEl(childSel);
57
+ return registry[childSel];
58
+ },
59
+ querySelectorAll() { return []; },
60
+ set innerHTML(v) { this._html = v; },
61
+ get innerHTML() { return this._html || ''; },
62
+ };
63
+ if (sel) registry[sel] = el;
64
+ return el;
65
+ };
46
66
 
47
67
  const documentObj = {
48
68
  body: { dataset: { page: 'home' }, appendChild() {} },
@@ -62,17 +82,34 @@ function bootPalette({ navItems = [], visibleIds = ['nav-standing', 'nav-work',
62
82
  const windowObj = { location: { hash: '', assign() {} }, addEventListener() {}, open() {} };
63
83
  const sandbox = {
64
84
  window: windowObj, document: documentObj, navigator: { platform: 'Win32', userAgent: 'test' },
65
- console,
85
+ console, setTimeout, clearTimeout, Promise, JSON, Number, String, Array, Object,
66
86
  };
87
+ if (fetchImpl) sandbox.fetch = fetchImpl;
67
88
  sandbox.globalThis = sandbox;
68
89
  vm.createContext(sandbox);
69
90
  vm.runInContext(PALETTE_SRC, sandbox, { filename: 'palette.js' });
70
91
  // Copy results across the realm boundary — a vm-created array has the sandbox's
71
92
  // Array.prototype, which deepStrictEqual rejects even when the values match.
72
93
  const P = windowObj.OTBPalette;
73
- return { compute: (q) => Array.from(P.compute(q), (e) => ({ ...e })) };
94
+ return {
95
+ compute: (q) => Array.from(P.compute(q), (e) => ({ ...e })),
96
+ contentEntry: (hit) => { const e = P.contentEntry(hit); return e ? { ...e } : e; },
97
+ open: () => P.open(),
98
+ close: () => P.close(),
99
+ listEl: () => registry['#palette-list'],
100
+ type(value) {
101
+ const el = registry['#palette-input'];
102
+ el.value = value;
103
+ el.fire('input');
104
+ },
105
+ };
74
106
  }
75
107
 
108
+ const tick = (ms = 0) => new Promise((r) => setTimeout(r, ms));
109
+ // Real timers (palette.js's debounce is a real setTimeout) — settle past the
110
+ // 300ms debounce plus a few microtask/macrotask turns for the fetch chain.
111
+ async function settleSearch() { await tick(400); }
112
+
76
113
  const NAV_FIXTURE = [
77
114
  { label: 'Home', href: '/' },
78
115
  { label: 'Tasks', href: '/work' },
@@ -207,3 +244,179 @@ test('every page that loads the shell also loads the palette', () => {
207
244
  const missing = withShell.filter((f) => !/src="\/builders\/palette\.js/.test(read(f)));
208
245
  assert.deepEqual(missing, [], 'Ctrl-K must work on every hall page');
209
246
  });
247
+
248
+ // ---- server-backed content search (task 1003460) ---------------------------
249
+
250
+ const TASK_HIT = { id: 1, source_kind: 'db', source_ref: 'task:1003460', title: "Ctrl-K searches the project's memory", heading_path: null };
251
+ const IDEA_HIT = { id: 2, source_kind: 'db', source_ref: 'idea:42', title: 'A homeless idea' };
252
+ const BLOCKER_HIT = { id: 3, source_kind: 'db', source_ref: 'blocker:7', title: 'Needs the owner' };
253
+ const DOC_HIT = { id: 4, source_kind: 'doc', source_ref: 'docs/adr/0060-gds-retrieval-layer.md', title: 'ADR 0060' };
254
+ const SESSION_LOG_HIT = { id: 5, source_kind: 'db', source_ref: 'session_log:9', title: 'Session log 9 · task 1003460' };
255
+
256
+ test('contentEntry resolves task/idea/blocker hits to real hall destinations', () => {
257
+ const P = bootPalette({ navItems: NAV_FIXTURE });
258
+ assert.deepEqual(
259
+ { label: P.contentEntry(TASK_HIT).label, href: P.contentEntry(TASK_HIT).href, overlay: !!P.contentEntry(TASK_HIT).overlay },
260
+ { label: "Ctrl-K searches the project's memory", href: '/task/1003460', overlay: false },
261
+ );
262
+ assert.deepEqual(
263
+ { href: P.contentEntry(IDEA_HIT).href, overlay: P.contentEntry(IDEA_HIT).overlay },
264
+ { href: '#/idea/42', overlay: true },
265
+ );
266
+ assert.deepEqual(
267
+ { href: P.contentEntry(BLOCKER_HIT).href, overlay: P.contentEntry(BLOCKER_HIT).overlay },
268
+ { href: '#/blocker/7', overlay: true },
269
+ );
270
+ });
271
+
272
+ test('contentEntry drops hits the hall has no page for (doc/session-log/etc)', () => {
273
+ const P = bootPalette({ navItems: NAV_FIXTURE });
274
+ assert.equal(P.contentEntry(DOC_HIT), null, 'a repo doc chunk has no hall page to jump to');
275
+ assert.equal(P.contentEntry(SESSION_LOG_HIT), null, 'a session-log chunk has no hall page to jump to');
276
+ assert.equal(P.contentEntry(null), null);
277
+ assert.equal(P.contentEntry({ source_kind: 'db', source_ref: 'commit:abc123' }), null, 'an unrecognized db ref resolves to nothing, not a crash');
278
+ });
279
+
280
+ test('contentEntry normalizes a dash the way cleanTitle() does everywhere else (V4, docs/voice.md)', () => {
281
+ // A task/idea/blocker title is quoted ledger data, not copy this project
282
+ // authored — V4 binds the dash ban to exactly that, enforced at render.
283
+ const P = bootPalette({ navItems: NAV_FIXTURE });
284
+ const hit = { source_kind: 'db', source_ref: 'task:99', title: 'V4.R18 — Ctrl-K searches the memory (follow-up)' };
285
+ assert.equal(P.contentEntry(hit).label, 'V4.R18, Ctrl-K searches the memory');
286
+ });
287
+
288
+ test('contentEntry truncates a pathologically long indexed title before it becomes markup', () => {
289
+ const P = bootPalette({ navItems: NAV_FIXTURE });
290
+ const longTitle = 'x'.repeat(400);
291
+ const hit = { source_kind: 'db', source_ref: 'task:1', title: longTitle };
292
+ const label = P.contentEntry(hit).label;
293
+ assert.ok(label.length <= 120, `label must be capped, got ${label.length} chars`);
294
+ assert.ok(label.endsWith('…'), 'a truncated label is marked as such');
295
+ });
296
+
297
+ test('typing prose merges in content matches from POST /api/bongos/search, on top of local matches', async () => {
298
+ const calls = [];
299
+ const fetchImpl = async (url, init) => {
300
+ calls.push({ url, body: JSON.parse(init.body) });
301
+ return { ok: true, json: async () => ({ query: 'ctrl-k memory', count: 1, results: [TASK_HIT] }) };
302
+ };
303
+ const P = bootPalette({ navItems: NAV_FIXTURE, fetchImpl });
304
+ P.open();
305
+ P.type('ctrl-k memory');
306
+ // Local compute() ran synchronously — pages/sections still searchable immediately.
307
+ assert.deepEqual(P.compute('ctrl-k memory'), [], 'sanity: no local page/section matches this prose');
308
+
309
+ await settleSearch();
310
+
311
+ assert.equal(calls.length, 1, 'exactly one debounced request, not one per keystroke');
312
+ assert.equal(calls[0].url, '/api/bongos/search');
313
+ assert.equal(calls[0].body.q, 'ctrl-k memory');
314
+ assert.equal(calls[0].body.source_kind, 'db', 'a doc/session-log/etc hit would only ever be dropped by contentEntry — ask the server to not spend the result budget on it');
315
+ assert.match(P.listEl().innerHTML, /Content/, 'the merged row carries the Content group');
316
+ assert.match(P.listEl().innerHTML, /Ctrl-K searches the project&#39;s memory/, 'and the task title from the server hit');
317
+ });
318
+
319
+ test('killing the search endpoint leaves the palette exactly as useful as today', async () => {
320
+ const fetchImpl = async () => { throw new Error('endpoint is down'); };
321
+ const P = bootPalette({ navItems: NAV_FIXTURE, fetchImpl });
322
+ P.open();
323
+ P.type('economy');
324
+ const beforeHtml = P.listEl().innerHTML;
325
+ assert.match(beforeHtml, /Economy/, 'the local page match rendered synchronously');
326
+
327
+ await settleSearch();
328
+
329
+ assert.equal(P.listEl().innerHTML, beforeHtml, 'a failed fetch changes nothing — no error row, no crash');
330
+ });
331
+
332
+ test('a 404 (endpoint removed) degrades the same as a network failure', async () => {
333
+ const fetchImpl = async () => ({ ok: false, status: 404, json: async () => ({ error: 'not_found' }) });
334
+ const P = bootPalette({ navItems: NAV_FIXTURE, fetchImpl });
335
+ P.open();
336
+ P.type('economy');
337
+ const beforeHtml = P.listEl().innerHTML;
338
+
339
+ await settleSearch();
340
+
341
+ assert.equal(P.listEl().innerHTML, beforeHtml);
342
+ });
343
+
344
+ test('a query under SEARCH_MIN_LEN never calls the endpoint at all', async () => {
345
+ let called = false;
346
+ const fetchImpl = async () => { called = true; return { ok: true, json: async () => ({ results: [] }) }; };
347
+ const P = bootPalette({ navItems: NAV_FIXTURE, fetchImpl });
348
+ P.open();
349
+ // 2 chars, not 1: the endpoint's least-selective case (falls through FTS to an
350
+ // unindexed trigram scan plus a zero-result corpusHealth() probe) is exactly
351
+ // the length this gate exists to keep off the wire.
352
+ P.type('ab');
353
+
354
+ await settleSearch();
355
+
356
+ assert.equal(called, false, 'two characters is not worth the endpoint\'s worst-case query plan');
357
+ });
358
+
359
+ test('closing the palette invalidates an in-flight search — it cannot land after close', async () => {
360
+ let resolveFetch;
361
+ const fetchImpl = () => new Promise((resolve) => { resolveFetch = resolve; });
362
+ const P = bootPalette({ navItems: NAV_FIXTURE, fetchImpl });
363
+ P.open();
364
+ P.type('ctrl-k memory');
365
+ await tick(350); // past the debounce — the request is now in flight
366
+ P.close();
367
+ resolveFetch({ ok: true, json: async () => ({ results: [TASK_HIT] }) });
368
+ await tick(20);
369
+
370
+ // isOpen() is false, so runSearch's own guard must have thrown the response away.
371
+ // (compute() is pure/local and would never carry a remote hit either way — the
372
+ // rendered DOM is the only place a landed-too-late merge could show up.)
373
+ assert.ok(!/Ctrl-K searches/.test(P.listEl().innerHTML), 'a response that lands after close must not render');
374
+ });
375
+
376
+ test('a duplicate destination (already resolved locally) is not appended twice', async () => {
377
+ const fetchImpl = async () => ({ ok: true, json: async () => ({ results: [TASK_HIT] }) });
378
+ const P = bootPalette({ navItems: NAV_FIXTURE, fetchImpl });
379
+ P.open();
380
+ P.type('1003460'); // id syntax already resolves this to the same /task/1003460 href
381
+ await settleSearch();
382
+
383
+ const rows = P.listEl().innerHTML.match(/data-i="\d+"/g) || [];
384
+ assert.equal(rows.length, 1, 'the server hit for the same task must not add a second row for the same href');
385
+ assert.match(P.listEl().innerHTML, /Task 1003460/, 'the id-syntax row is the one that stays');
386
+ });
387
+
388
+ test('a repeated query within one open() replays the cache instead of re-fetching', async () => {
389
+ const calls = [];
390
+ const fetchImpl = async () => { calls.push(1); return { ok: true, json: async () => ({ results: [TASK_HIT] }) }; };
391
+ const P = bootPalette({ navItems: NAV_FIXTURE, fetchImpl });
392
+ P.open();
393
+ P.type('ctrl-k memory');
394
+ await settleSearch();
395
+ assert.equal(calls.length, 1);
396
+
397
+ // Backspace, then retype back to the exact same query (the common case: a
398
+ // reader pauses mid-edit and returns to what they had).
399
+ P.type('ctrl-k mem');
400
+ P.type('ctrl-k memory');
401
+ await settleSearch();
402
+
403
+ assert.equal(calls.length, 1, 'the identical query was served from the per-open cache, not re-fetched');
404
+ assert.match(P.listEl().innerHTML, /Ctrl-K searches the project&#39;s memory/, 'the cached hit still renders');
405
+ });
406
+
407
+ test('closing and reopening clears the cache — a fresh open may see new DB state', async () => {
408
+ const calls = [];
409
+ const fetchImpl = async () => { calls.push(1); return { ok: true, json: async () => ({ results: [TASK_HIT] }) }; };
410
+ const P = bootPalette({ navItems: NAV_FIXTURE, fetchImpl });
411
+ P.open();
412
+ P.type('ctrl-k memory');
413
+ await settleSearch();
414
+ assert.equal(calls.length, 1);
415
+
416
+ P.close();
417
+ P.open();
418
+ P.type('ctrl-k memory');
419
+ await settleSearch();
420
+
421
+ assert.equal(calls.length, 2, 'a fresh open must not trust a memo carried over from the previous one');
422
+ });
package/tests/helpers.mjs CHANGED
@@ -58,7 +58,7 @@ export function makeSqlAwareClient(handler) {
58
58
  // tests/ is PUBLISHABLE, but the paths some tests assert are on
59
59
  // publish-manifest.js's PROTECTED_TOPOLOGY_EXCLUDE list (`.claude/hooks/`,
60
60
  // `.claude/settings.json`, `infra/`, `.github/`, `scripts/deploy/`) or are
61
- // instance-only content (`devbox-app/`, `config/<instance>.json`). A neutral
61
+ // instance-only content (`config/<instance>.json`). A neutral
62
62
  // core checkout carries none of it, so an unconditional load CRASHED the file
63
63
  // at import and red-X'd the unit gate for every consumer.
64
64
  //
@@ -3,7 +3,7 @@
3
3
  // tests/ is publishable, but some tests assert paths the neutral core deliberately does
4
4
  // not carry: publish-manifest.js's PROTECTED_TOPOLOGY_EXCLUDE (`.claude/hooks/`,
5
5
  // `.claude/settings.json`, `infra/`, `.github/`, `scripts/deploy/`) plus instance-only
6
- // content (`devbox-app/`, `config/<instance>.json`, an uninstalled module). Those files
6
+ // content (`config/<instance>.json`, an uninstalled module). Those files
7
7
  // used to CRASH at import and red-X the unit gate for every consumer; they now skip
8
8
  // cleanly via the helpers in tests/helpers.mjs and run in full wherever the topology is
9
9
  // materialized.
@@ -33,7 +33,7 @@ const ALLOWED = {
33
33
  allow_rule_guard: '.claude/hooks/allow-rule-guard.js (package-excluded)',
34
34
  art_stage: 'scripts/gds/art-stage.js (art-pipeline tooling, not in the core)',
35
35
  bongos_downloads: 'config/bongos-cli.json (instance content)',
36
- box_ship_permissions: '.claude/settings.json (package-excluded)',
36
+ cli_allowlist_permissions: '.claude/settings.json (package-excluded)',
37
37
  conductor: '.claude/hooks/conductor.js (package-excluded)',
38
38
  deploy_instance: 'scripts/deploy/deploy-instance.sh (host topology)',
39
39
  eviction_policy: '.claude/hooks/eviction-policy.js (package-excluded)',
@@ -46,8 +46,8 @@ t('parseMigrationName: core_NNN_slug.sql', () => {
46
46
  });
47
47
 
48
48
  t('parseMigrationName: multi-word module prefix splits at the number, not greedily', () => {
49
- const p = ns.parseMigrationName('dev_box_003_add_col.sql');
50
- assert.equal(p.prefix, 'dev_box', 'dev_box_003 → prefix dev_box, not dev');
49
+ const p = ns.parseMigrationName('platform_identity_003_add_col.sql');
50
+ assert.equal(p.prefix, 'platform_identity', 'platform_identity_003 → prefix platform_identity, not platform');
51
51
  assert.equal(p.number, 3);
52
52
  assert.equal(p.slug, 'add_col');
53
53
  });
@@ -114,10 +114,10 @@ t('filenameViolation: NEW module migration must be <key>_-prefixed', () => {
114
114
  });
115
115
 
116
116
  t('filenameViolation: hyphenated module key maps to underscore prefix', () => {
117
- assert.equal(ns.sqlNamespace('dev-box'), 'dev_box');
118
- assert.equal(ns.filenameViolation('module', 'dev_box_003_x.sql', { moduleKey: 'dev-box' }), null);
119
- const v = ns.filenameViolation('module', '003_x.sql', { moduleKey: 'dev-box' });
120
- assert.ok(v && /dev_box_-prefixed|dev_box_003_x/.test(v));
117
+ assert.equal(ns.sqlNamespace('platform-identity'), 'platform_identity');
118
+ assert.equal(ns.filenameViolation('module', 'platform_identity_003_x.sql', { moduleKey: 'platform-identity' }), null);
119
+ const v = ns.filenameViolation('module', '003_x.sql', { moduleKey: 'platform-identity' });
120
+ assert.ok(v && /platform_identity_-prefixed|platform_identity_003_x/.test(v));
121
121
  });
122
122
 
123
123
  // ---- stemCollisions --------------------------------------------------------
@@ -16,8 +16,8 @@ const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
16
16
  // removing/renaming a key is normally a MAJOR bump (a breaking change to every
17
17
  // module). Exception (BV1.R68 / ADR 0091 §3): relocating a DOMAIN query whose
18
18
  // sole in-repo consumer moves in lockstep is treated as MINOR — e.g.
19
- // `setBuilderBoxBlocked` left for modules/dev-box/db.js at 1.5.0, when `withTx`
20
- // was added; no enabled module at its pinned coreVersion lost a capability.
19
+ // a per-builder setter left the kernel for its own module's db.js at 1.5.0, when
20
+ // `withTx` was added; no enabled module at its pinned coreVersion lost a capability.
21
21
  const PUBLISHED_SURFACE = [
22
22
  'CORE_VERSION',
23
23
  'requireBuilder', 'requireRank', 'rankMeetsThreshold', 'requireBfgPrincipal', 'allowBoxScope', 'resolveSession',
@@ -63,11 +63,11 @@ test('scaffoldModule: writes a valid, loader-discoverable module with no core ed
63
63
 
64
64
  test('scaffoldModule: a kebab-case key produces matching dir, route file, and title', () => {
65
65
  const root = tmpModulesRoot();
66
- const { dir, manifest } = mod.scaffoldModule({ key: 'dev-box', modulesDir: root, core: '1.0.0' });
67
- assert.equal(manifest.title, 'Dev Box');
68
- assert.deepEqual(manifest.contributes.routes, ['dev-box']);
69
- assert.ok(fs.existsSync(path.join(dir, 'routes', 'dev-box.js')));
70
- assert.match(fs.readFileSync(path.join(dir, 'routes', 'dev-box.js'), 'utf8'), /devBoxRoutes/);
66
+ const { dir, manifest } = mod.scaffoldModule({ key: 'copy-desk', modulesDir: root, core: '1.0.0' });
67
+ assert.equal(manifest.title, 'Copy Desk');
68
+ assert.deepEqual(manifest.contributes.routes, ['copy-desk']);
69
+ assert.ok(fs.existsSync(path.join(dir, 'routes', 'copy-desk.js')));
70
+ assert.match(fs.readFileSync(path.join(dir, 'routes', 'copy-desk.js'), 'utf8'), /copyDeskRoutes/);
71
71
  });
72
72
 
73
73
  test('scaffoldModule: rejects an invalid key', () => {
@@ -12,11 +12,11 @@ const { satisfies, isValidRange, parseVersion } = require(path.join(ROOT, 'src',
12
12
  const { validateManifest, assertValidManifest } = require(path.join(ROOT, 'src', 'module-loader', 'manifest-schema.js'));
13
13
 
14
14
  const baseManifest = () => ({
15
- key: 'dev-box',
16
- title: 'Dev boxes',
17
- description: 'Per-builder cloud dev environments.',
15
+ key: 'discord',
16
+ title: 'Discord',
17
+ description: 'A bridge to the team chat.',
18
18
  version: '1.0.0', coreVersion: '^1.0.0',
19
- contributes: { routes: ['box'], pollers: ['box-lifecycle'], uiSections: ['boxes'] },
19
+ contributes: { routes: ['discord'], pollers: ['discord-bot'], uiSections: ['chat'] },
20
20
  });
21
21
 
22
22
  // --- semver ---
@@ -91,7 +91,7 @@ test('missing required fields each produce an error', () => {
91
91
  });
92
92
 
93
93
  test('bad key format is rejected', () => {
94
- const r = validateManifest({ ...baseManifest(), key: 'Dev_Box' });
94
+ const r = validateManifest({ ...baseManifest(), key: 'Discord_Bot' });
95
95
  assert.equal(r.valid, false);
96
96
  assert.ok(r.errors.some((e) => e.startsWith('key')));
97
97
  });
@@ -112,13 +112,13 @@ test('version must be an exact X.Y.Z — a range or junk is rejected (task 10037
112
112
  });
113
113
 
114
114
  test('unknown contributes key is rejected (typo guard)', () => {
115
- const r = validateManifest({ ...baseManifest(), contributes: { routs: ['box'] } });
115
+ const r = validateManifest({ ...baseManifest(), contributes: { routs: ['discord'] } });
116
116
  assert.equal(r.valid, false);
117
117
  assert.ok(r.errors.some((e) => e.includes('contributes.routs')));
118
118
  });
119
119
 
120
120
  test('non-array contributes.routes is rejected', () => {
121
- const r = validateManifest({ ...baseManifest(), contributes: { routes: 'box' } });
121
+ const r = validateManifest({ ...baseManifest(), contributes: { routes: 'discord' } });
122
122
  assert.equal(r.valid, false);
123
123
  assert.ok(r.errors.some((e) => e.includes('contributes.routes')));
124
124
  });
@@ -296,7 +296,7 @@ test('every bundled module declares a maintenance posture (the catalog has no un
296
296
  test('declarativeSeams: a valid exemption naming a declared seam + entry passes', () => {
297
297
  const r = validateManifest({
298
298
  ...baseManifest(),
299
- declarativeSeams: { pollers: ['box-lifecycle'], uiSections: ['boxes'] },
299
+ declarativeSeams: { pollers: ['discord-bot'], uiSections: ['chat'] },
300
300
  });
301
301
  assert.equal(r.valid, true, r.errors.join('; '));
302
302
  });
@@ -304,8 +304,8 @@ test('declarativeSeams: a valid exemption naming a declared seam + entry passes'
304
304
  test('declarativeSeams: an exemption for a seam the module does not declare is an ERROR', () => {
305
305
  const r = validateManifest({
306
306
  ...baseManifest(),
307
- contributes: { routes: ['box'] }, // no pollers declared
308
- declarativeSeams: { pollers: ['box-lifecycle'] },
307
+ contributes: { routes: ['discord'] }, // no pollers declared
308
+ declarativeSeams: { pollers: ['discord-bot'] },
309
309
  });
310
310
  assert.equal(r.valid, false);
311
311
  assert.ok(r.errors.some((e) => /does not declare contributes\.pollers/.test(e)), r.errors.join('; '));
@@ -314,10 +314,10 @@ test('declarativeSeams: an exemption for a seam the module does not declare is a
314
314
  test('declarativeSeams: an exemption naming an ENTRY the module does not list is an ERROR (the typo case)', () => {
315
315
  const r = validateManifest({
316
316
  ...baseManifest(),
317
- declarativeSeams: { pollers: ['box-lifecyle'] }, // transposed letters
317
+ declarativeSeams: { pollers: ['discord-bto'] }, // transposed letters
318
318
  });
319
319
  assert.equal(r.valid, false);
320
- assert.ok(r.errors.some((e) => /"box-lifecyle" is not in contributes\.pollers/.test(e)), r.errors.join('; '));
320
+ assert.ok(r.errors.some((e) => /"discord-bto" is not in contributes\.pollers/.test(e)), r.errors.join('; '));
321
321
  });
322
322
 
323
323
  test('declarativeSeams: the per-entry check spans EVERY seam, not just bare string arrays', () => {
@@ -344,7 +344,7 @@ test('declarativeSeams: an unknown seam key is an ERROR, not a silent no-op', ()
344
344
 
345
345
  test('declarativeSeams: shape errors — non-object block, non-string-array entries', () => {
346
346
  assert.equal(validateManifest({ ...baseManifest(), declarativeSeams: ['pollers'] }).valid, false);
347
- assert.equal(validateManifest({ ...baseManifest(), declarativeSeams: { pollers: 'box-lifecycle' } }).valid, false);
347
+ assert.equal(validateManifest({ ...baseManifest(), declarativeSeams: { pollers: 'discord-bot' } }).valid, false);
348
348
  assert.equal(validateManifest({ ...baseManifest(), declarativeSeams: { pollers: [''] } }).valid, false);
349
349
  });
350
350
 
@@ -91,9 +91,9 @@ test('checkRouteRanks: an absent modules/ dir leaves the core result clean (byte
91
91
 
92
92
  // ---- the grade-time trigger fires on a module route change ----
93
93
  test('ROUTE_FILE_RE matches a module route path (a module route change triggers the rank pre-pass)', () => {
94
- assert.match('modules/dev-box/routes/box.js', rrc.ROUTE_FILE_RE);
94
+ assert.match('modules/discord/routes/discord.js', rrc.ROUTE_FILE_RE);
95
95
  assert.match('src/bongos/routes/tasks.js', rrc.ROUTE_FILE_RE); // core still matches
96
- assert.ok(!rrc.ROUTE_FILE_RE.test('modules/dev-box/module.json'), 'a non-route module file does not trigger the pre-pass');
96
+ assert.ok(!rrc.ROUTE_FILE_RE.test('modules/discord/module.json'), 'a non-route module file does not trigger the pre-pass');
97
97
  });
98
98
 
99
99
  // ---- a rank DRIFT on a pinned MODULE route reds the build (BV1.R61) ----
@@ -79,7 +79,7 @@ test('1002857: POST /modules/:key/enable rejects a non-boolean before touching t
79
79
  // is why the next assertion matters more than this one.
80
80
  const router = buildModulesRouter();
81
81
  const res = await invoke(handlerFor(router, 'post', '/modules/:key/enable'), {
82
- params: { key: 'dev-box' }, query: {}, body: { enabled: 'yes-please' }, builder: { id: 1 },
82
+ params: { key: 'discord' }, query: {}, body: { enabled: 'yes-please' }, builder: { id: 1 },
83
83
  });
84
84
  assert.equal(res.body.error, 'invalid_body');
85
85
  });
@@ -137,7 +137,7 @@ await t('no live signals → discord/cli both uncleared, and no retired box step
137
137
  assert.equal(r.status, 200);
138
138
  assert.equal(stepCleared(r.json, 'join_discord'), false);
139
139
  assert.equal(stepCleared(r.json, 'cli_setup'), false);
140
- assert.equal(stepCleared(r.json, 'box_connect'), undefined, 'the dev-box step left the checklist (task 1004310)');
140
+ assert.equal(stepCleared(r.json, 'box_connect'), undefined, 'the retired box_connect step is no longer on the checklist (task 1004310)');
141
141
  });
142
142
 
143
143
  await t('discord linked → join_discord clears from ITS OWN signal [the scootermack regression]', async () => {
@@ -139,7 +139,7 @@ test('pasting a token for instance B PRESERVES instance A — the bug this close
139
139
  assert.equal(stored.token, 'B'.repeat(64));
140
140
 
141
141
  // 3. The active pointer still follows the paste, in the same place every existing
142
- // reader (skills, hooks, the dev box) looks. The fix is additive or it is a
142
+ // reader (skills, hooks) looks. The fix is additive or it is a
143
143
  // different, quieter outage.
144
144
  const pointer = readJson(active);
145
145
  assert.equal(pointer.api_base, base, 'the active pointer must follow the paste');
@@ -162,7 +162,7 @@ test('the exempt list: the door, the manifest, the probes, the downloads — on
162
162
  '/api/gds/instance', '/api/bongos/instance', '/api/gds/healthz',
163
163
  '/api/bongos/provisioning/tls-check', '/api/gds/provisioning/health',
164
164
  '/healthz', '/version', '/metrics', '/__stealth',
165
- '/downloads/bongos/install.sh', '/downloads/devbox/latest.json',
165
+ '/downloads/bongos/install.sh', '/downloads/bongos/latest.json',
166
166
  // Express routes case-insensitively and with a trailing slash — the list sees what the router sees
167
167
  '/API/GDS/INSTANCE', '/api/gds/instance/', '/HEALTHZ', '/api/bongos/auth/',
168
168
  ]) assert.equal(isExempt(p), true, `${p} must answer a stranger`);