@bongos/core 1.20.9 → 1.20.11

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 (91) hide show
  1. package/.bongos-core.json +101 -96
  2. package/.claude/skills/ask-for-help/SKILL.md +6 -0
  3. package/.claude/skills/backlog-review/SKILL.md +6 -0
  4. package/.claude/skills/blocker-review/SKILL.md +6 -0
  5. package/.claude/skills/blocker-solve/SKILL.md +6 -0
  6. package/.claude/skills/bug-triage/SKILL.md +6 -0
  7. package/.claude/skills/builder-backup/SKILL.md +6 -0
  8. package/.claude/skills/builder-claim/SKILL.md +6 -0
  9. package/.claude/skills/builder-cost/SKILL.md +6 -0
  10. package/.claude/skills/builder-end/SKILL.md +6 -0
  11. package/.claude/skills/builder-exit/SKILL.md +6 -0
  12. package/.claude/skills/builder-key/SKILL.md +6 -0
  13. package/.claude/skills/builder-reauth/SKILL.md +6 -0
  14. package/.claude/skills/builder-redteam/SKILL.md +6 -0
  15. package/.claude/skills/builder-release/SKILL.md +6 -0
  16. package/.claude/skills/builder-sequence/SKILL.md +6 -0
  17. package/.claude/skills/builder-setup/SKILL.md +6 -0
  18. package/.claude/skills/builder-ship/SKILL.md +6 -0
  19. package/.claude/skills/builder-stage/SKILL.md +6 -0
  20. package/.claude/skills/builder-start/SKILL.md +6 -0
  21. package/.claude/skills/collab-review/SKILL.md +6 -0
  22. package/.claude/skills/design/SKILL.md +6 -0
  23. package/.claude/skills/design-sync/SKILL.md +6 -0
  24. package/.claude/skills/feedback/SKILL.md +6 -0
  25. package/.claude/skills/figma-design-sync/SKILL.md +6 -0
  26. package/.claude/skills/fix-task/SKILL.md +6 -0
  27. package/.claude/skills/goal-create/SKILL.md +6 -0
  28. package/.claude/skills/goal-review/SKILL.md +6 -0
  29. package/.claude/skills/goal-uat/SKILL.md +6 -0
  30. package/.claude/skills/grade-audit/SKILL.md +6 -0
  31. package/.claude/skills/grade-recover/SKILL.md +6 -0
  32. package/.claude/skills/grader-health/SKILL.md +6 -0
  33. package/.claude/skills/idea-triage/SKILL.md +6 -0
  34. package/.claude/skills/merge-mode/SKILL.md +6 -0
  35. package/.claude/skills/new-project/SKILL.md +6 -0
  36. package/.claude/skills/otb-character-review/SKILL.md +6 -0
  37. package/.claude/skills/otb-design-review/SKILL.md +6 -0
  38. package/.claude/skills/otb-feedback-capture/SKILL.md +6 -0
  39. package/.claude/skills/otb-figma-sync/SKILL.md +6 -0
  40. package/.claude/skills/otb-tile-generate/SKILL.md +6 -0
  41. package/.claude/skills/planning-session/SKILL.md +6 -0
  42. package/.claude/skills/priority-session/SKILL.md +6 -0
  43. package/.claude/skills/read-session-export/SKILL.md +6 -0
  44. package/.claude/skills/recall/SKILL.md +6 -0
  45. package/.claude/skills/scan-before-install/SKILL.md +6 -0
  46. package/.claude/skills/session-handoff/SKILL.md +6 -0
  47. package/.claude/skills/ship-check/SKILL.md +6 -0
  48. package/.claude/skills/status/SKILL.md +6 -0
  49. package/.claude/skills/strand-fix/SKILL.md +6 -0
  50. package/.claude/skills/tweak/SKILL.md +6 -0
  51. package/.claude/skills/worktree-clean/SKILL.md +6 -0
  52. package/clients/bongos-client/README.md +1 -1
  53. package/clients/bongos-client/bongos-client.global.js +2 -0
  54. package/clients/bongos-client/index.cjs +2 -0
  55. package/clients/bongos-client/index.d.ts +3 -0
  56. package/clients/bongos-client/index.mjs +2 -0
  57. package/docs/api/openapi.json +50 -3
  58. package/docs/api-reference.md +3 -2
  59. package/docs/copy-inventory.md +60 -52
  60. package/docs/copy-registry.json +126 -54
  61. package/docs/module-api-changelog.md +4 -0
  62. package/docs/page-inventory.json +2 -1
  63. package/docs/page-readings.json +131 -119
  64. package/modules/hall-ui/public/panel.css +4 -4
  65. package/modules/hall-ui/public/settings-specialities.js +139 -2
  66. package/modules/hall-ui/public/settings.css +26 -0
  67. package/modules/hall-ui/public/settings.html +4 -0
  68. package/modules/hall-ui/public/settings.states.json +1 -0
  69. package/modules/hall-ui/records/panel-family.md +2 -0
  70. package/modules/specialities/routes/specialities.js +16 -0
  71. package/modules/ui-design/skills/brandkit/SKILL.md +6 -0
  72. package/modules/ui-design/skills/design-taste-frontend/SKILL.md +6 -0
  73. package/modules/ui-design/skills/design-taste-frontend-v1/SKILL.md +6 -0
  74. package/modules/ui-design/skills/gpt-taste/SKILL.md +6 -0
  75. package/modules/ui-design/skills/high-end-visual-design/SKILL.md +6 -0
  76. package/modules/ui-design/skills/image-to-code/SKILL.md +6 -0
  77. package/modules/ui-design/skills/imagegen-frontend-mobile/SKILL.md +6 -0
  78. package/modules/ui-design/skills/imagegen-frontend-web/SKILL.md +6 -0
  79. package/modules/ui-design/skills/impeccable/SKILL.md +6 -0
  80. package/modules/ui-design/skills/industrial-brutalist-ui/SKILL.md +6 -0
  81. package/modules/ui-design/skills/minimalist-ui/SKILL.md +6 -0
  82. package/modules/ui-design/skills/redesign-existing-projects/SKILL.md +6 -0
  83. package/modules/ui-design/skills/stitch-design-taste/SKILL.md +6 -0
  84. package/modules/ui-design/skills/style/SKILL.md +6 -0
  85. package/package-lock.json +2 -2
  86. package/package.json +1 -1
  87. package/release-notes.json +12 -0
  88. package/scripts/hall-preview/server.js +4 -0
  89. package/src/module-api.js +1 -1
  90. package/tests/speciality_offer_ui.mjs +185 -0
  91. package/tests/speciality_skills.mjs +51 -0
@@ -0,0 +1,185 @@
1
+ // tests/speciality_offer_ui.mjs — a speciality's OWNER chooses what it offers,
2
+ // from the hall (task 1004404; ADR 0310 §1).
3
+ //
4
+ // Before this only the API could set a speciality's offered skills, so nothing
5
+ // made in the hall offered any and the adoption walkthrough never appeared. These
6
+ // pin that the owner can choose on create AND afterwards, that each choice shows
7
+ // the skill's plain description, that only installed skills are choices, and that
8
+ // a skill removed since is named rather than silently kept.
9
+ //
10
+ // Like tests/speciality_walkthrough_ui.mjs this EXECUTES the real
11
+ // modules/hall-ui/public/settings-specialities.js in a vm under a minimal DOM
12
+ // stub against a fake API, and drives the delegated handlers the page wires.
13
+ //
14
+ // Run: node tests/speciality_offer_ui.mjs
15
+ import assert from 'node:assert/strict';
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import vm from 'node:vm';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { makeRunner } from './helpers.mjs';
21
+
22
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
23
+ const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/settings-specialities.js'), 'utf8');
24
+ const HTML = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/settings.html'), 'utf8');
25
+ const { test, summary } = makeRunner();
26
+
27
+ const INSTALLED = [
28
+ { name: 'builder-cost', does: 'Records money spent on the project.', cost: 'Free. It only adds an entry.' },
29
+ { name: 'recall', does: 'Searches everything the project already knows.', cost: 'Free, and it changes nothing.' },
30
+ { name: 'status', does: 'Shows how far along a version is.', cost: 'Free, and it changes nothing.' },
31
+ ];
32
+
33
+ const mine = (over = {}) => ({ id: 12, name: 'Pricing', discipline: 'engineer', summary: '', contract_md: '', visibility: 'private', adoptable: true, mine: true, skills: ['recall', 'retired-skill'], ...over });
34
+ const theirs = (over = {}) => ({ id: 9, name: 'Economics', discipline: 'ideator', summary: '', contract_md: '', visibility: 'public', adoptable: true, mine: false, skills: ['recall'], ...over });
35
+
36
+ // The checked boxes a form would submit, read off the rendered markup.
37
+ const checkedIn = (html) => [...html.matchAll(/<input type="checkbox" name="skill" value="([^"]+)" checked/g)].map((m) => m[1]);
38
+ const fakeForm = (dataset, values) => ({
39
+ dataset,
40
+ querySelectorAll: (sel) => (sel === 'input[name="skill"]:checked' ? values.map((value) => ({ value })) : []),
41
+ });
42
+
43
+ async function boot({ catalog = [mine(), theirs()] } = {}) {
44
+ const requests = [];
45
+ const toasts = [];
46
+ const els = new Map();
47
+ const handlers = new Map();
48
+ const makeEl = (id) => ({
49
+ id, hidden: id === 'specialities-scroll', innerHTML: '', textContent: '', elements: {}, dataset: {}, open: false,
50
+ addEventListener(type, fn) { handlers.set(`${id}:${type}`, fn); },
51
+ });
52
+ for (const id of ['specialities-scroll', 'specialities-rows', 'speciality-suggestions', 'specialities-status', 'speciality-create', 'speciality-create-offer', 'speciality-create-skills']) els.set(id, makeEl(id));
53
+ // The create form's fields, as settings.html declares them.
54
+ const create = els.get('speciality-create');
55
+ const field = (value) => ({ value, addEventListener() {} });
56
+ create.elements = { name: field(''), discipline: field(''), summary: field(''), contract_md: field(''), visibility: field('private'), sellable: { checked: false, disabled: false, addEventListener() {} } };
57
+ create.reset = () => { create.elements.name.value = ''; };
58
+ create.querySelectorAll = () => [];
59
+
60
+ const fetch = async (url, opts = {}) => {
61
+ const method = opts.method || 'GET';
62
+ const p = url.replace('/api/bongos', '');
63
+ requests.push({ method, path: p, body: opts.body ? JSON.parse(opts.body) : undefined });
64
+ let body = {};
65
+ if (method === 'GET' && p === '/specialities') body = { specialities: catalog };
66
+ else if (method === 'GET' && p === '/me/specialities') body = { adoptions: [] };
67
+ else if (method === 'GET' && p === '/specialities/offered') body = { suggestions: [] };
68
+ else if (method === 'GET' && p === '/specialities/installed-skills') body = { skills: INSTALLED };
69
+ else if (method === 'PATCH') body = { speciality: { id: 12 } };
70
+ else if (method === 'POST' && p === '/specialities') body = { speciality: { id: 13 } };
71
+ return { ok: true, status: 200, json: async () => body };
72
+ };
73
+
74
+ const document = { readyState: 'complete', getElementById: (id) => els.get(id) || null, addEventListener() {}, querySelector: () => null };
75
+ const window = {
76
+ OTB: {
77
+ escapeHtml: (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'),
78
+ $: () => null, toast: (m) => toasts.push(m), roleAxisLabel: () => 'Role',
79
+ },
80
+ location: { assign() {} },
81
+ };
82
+ vm.runInNewContext(SRC, { window, document, fetch, JSON, Promise, Map, Set, String, Number, Array, Object, Math, RegExp, console });
83
+ const settle = async () => { for (let i = 0; i < 8; i++) await new Promise((r) => setTimeout(r, 0)); };
84
+ await settle();
85
+
86
+ const rows = els.get('specialities-rows');
87
+ async function click(act, id) {
88
+ const btn = { dataset: { act, id: String(id) } };
89
+ handlers.get('specialities-rows:click')({ target: { closest: (sel) => (sel === 'button[data-act]' ? btn : null) } });
90
+ await settle();
91
+ }
92
+ async function submitOffer(id, values) {
93
+ const form = fakeForm({ id: String(id) }, values);
94
+ handlers.get('specialities-rows:submit')({ target: { closest: (sel) => (sel === 'form.speciality-offer' ? form : null) }, preventDefault() {} });
95
+ await settle();
96
+ }
97
+ return { els, rows, requests, toasts, click, submitOffer, handlers, create, settle };
98
+ }
99
+
100
+ const writes = (h) => h.requests.filter((r) => r.method !== 'GET');
101
+ const installedReads = (h) => h.requests.filter((r) => r.path === '/specialities/installed-skills').length;
102
+
103
+ await test('only a speciality you own has an "Offered skills" control', async () => {
104
+ const h = await boot();
105
+ const buttons = [...h.rows.innerHTML.matchAll(/data-act="offer" data-id="(\d+)"/g)].map((m) => m[1]);
106
+ assert.deepEqual(buttons, ['12']);
107
+ });
108
+
109
+ await test('opening it lists every installed skill with its plain description and cost, the current offer ticked', async () => {
110
+ const h = await boot();
111
+ assert.equal(installedReads(h), 0, 'nothing is fetched until the owner asks');
112
+ await h.click('offer', 12);
113
+ assert.equal(installedReads(h), 1);
114
+ const html = h.rows.innerHTML;
115
+ for (const k of INSTALLED) {
116
+ assert.ok(html.includes(`value="${k.name}"`), `${k.name} is a choice`);
117
+ assert.ok(html.includes(k.does), `${k.name} shows what it does`);
118
+ assert.ok(html.includes(k.cost), `${k.name} shows what it costs`);
119
+ }
120
+ assert.deepEqual(checkedIn(html), ['recall'], 'what it offers today is ticked');
121
+ assert.match(html, /Offering a skill turns nothing on/);
122
+ });
123
+
124
+ await test('a skill it offers that the instance no longer has is named, not offered as a choice', async () => {
125
+ const h = await boot();
126
+ await h.click('offer', 12);
127
+ assert.ok(!h.rows.innerHTML.includes('value="retired-skill"'), 'not a checkbox');
128
+ assert.match(h.rows.innerHTML, /No longer on this project, so saving drops it from the offer: <code>retired-skill<\/code>/);
129
+ });
130
+
131
+ await test('saving sends exactly the ticked skills as one PATCH, and nothing is written before', async () => {
132
+ const h = await boot();
133
+ await h.click('offer', 12);
134
+ assert.equal(writes(h).length, 0);
135
+ await h.submitOffer(12, ['recall', 'status']);
136
+ const w = writes(h);
137
+ assert.equal(w.length, 1);
138
+ assert.equal(w[0].method, 'PATCH');
139
+ assert.equal(w[0].path, '/specialities/12');
140
+ assert.deepEqual(w[0].body, { skills: ['recall', 'status'] });
141
+ assert.ok(h.toasts.some((m) => /offers 2 skills/.test(m)));
142
+ assert.ok(!/class="speciality-offer"/.test(h.rows.innerHTML), 'the editor closes on save');
143
+ });
144
+
145
+ await test('offering nothing is a choice too', async () => {
146
+ const h = await boot();
147
+ await h.click('offer', 12);
148
+ await h.submitOffer(12, []);
149
+ assert.deepEqual(writes(h)[0].body, { skills: [] });
150
+ });
151
+
152
+ await test('the installed list is read once per page, however often the editor opens', async () => {
153
+ const h = await boot();
154
+ await h.click('offer', 12);
155
+ await h.click('offer', 12); // close
156
+ await h.click('offer', 12); // open again
157
+ assert.equal(installedReads(h), 1);
158
+ });
159
+
160
+ await test('the create form offers the same choices, filled on first open, and sends them with the new speciality', async () => {
161
+ const h = await boot();
162
+ const box = h.els.get('speciality-create-offer');
163
+ const host = h.els.get('speciality-create-skills');
164
+ box.open = true;
165
+ h.handlers.get('speciality-create-offer:toggle')();
166
+ await h.settle();
167
+ for (const k of INSTALLED) assert.ok(host.innerHTML.includes(`value="${k.name}"`) && host.innerHTML.includes(k.does));
168
+ assert.deepEqual(checkedIn(host.innerHTML), [], 'a new speciality starts offering nothing');
169
+ h.create.elements.name.value = 'New one';
170
+ h.create.elements.discipline.value = 'engineer';
171
+ h.create.querySelectorAll = (sel) => (sel === 'input[name="skill"]:checked' ? [{ value: 'status' }] : []);
172
+ h.handlers.get('speciality-create:submit')({ preventDefault() {} });
173
+ await h.settle();
174
+ const post = writes(h).find((r) => r.method === 'POST' && r.path === '/specialities');
175
+ assert.ok(post, 'created');
176
+ assert.deepEqual(post.body.skills, ['status']);
177
+ });
178
+
179
+ await test('settings.html carries the create form\'s picker inside the form it submits with', () => {
180
+ const form = HTML.slice(HTML.indexOf('<form id="speciality-create"'), HTML.indexOf('</form>', HTML.indexOf('<form id="speciality-create"')));
181
+ assert.match(form, /<details id="speciality-create-offer"/);
182
+ assert.match(form, /id="speciality-create-skills"/);
183
+ });
184
+
185
+ summary();
@@ -73,6 +73,48 @@ await test('every skill in the real tree reads a real description — none is a
73
73
  assert.deepEqual(bad, [], `these would teach nothing in the walkthrough: ${bad.join(', ')}`);
74
74
  });
75
75
 
76
+ // task 1004403: every skill a speciality could offer — every core skill and
77
+ // every module's, enabled here or not — carries its own people-facing lines, so
78
+ // the walkthrough never has to derive them and a new skill cannot arrive without.
79
+ const { JARGON_TERMS } = require('../modules/sessions/accessibility.js');
80
+ // The words task 1004403 named as the ones the derived text leaked, plus the
81
+ // repo nouns a newcomer would not know.
82
+ const SKILL_JARGON = ['DB', 'grep', 'grepping', 'rank-scoped', 'status=', 'residue', 'criterion', 'criteria', 'touches[]', 'branch', 'commit', 'diff', 'SHA', 'CI', 'repo', 'PR'];
83
+ const everySkill = () => skills.installedSkills({ coreRoot: ROOT, isModuleEnabled: () => true });
84
+
85
+ await test('every installable skill carries plain, reach-for and cost — none is derived', () => {
86
+ const all = [...everySkill().values()];
87
+ assert.ok(all.length >= 60, `the whole tree was read (${all.length})`);
88
+ const missing = all.flatMap((s) => ['plain', 'reachFor', 'cost'].filter((k) => !s[k] || s[k].length < 15).map((k) => `${s.name}.${k}`));
89
+ assert.deepEqual(missing, [], 'add plain: / reach-for: / cost: to the SKILL.md frontmatter, written for someone with no engineering background');
90
+ const derived = all.filter((s) => { const d = skills.explainSkill(s).derived; return d.does || d.reachFor || d.cost; }).map((s) => s.name);
91
+ assert.deepEqual(derived, [], 'the walkthrough shows the author\'s own words for every skill');
92
+ });
93
+
94
+ await test('the people-facing lines stay out of the vocabulary they exist to translate', () => {
95
+ const banned = [...JARGON_TERMS, ...SKILL_JARGON];
96
+ const hits = [];
97
+ for (const s of everySkill().values()) {
98
+ for (const k of ['plain', 'reachFor', 'cost']) {
99
+ for (const term of banned) {
100
+ const re = new RegExp(`(^|[^A-Za-z])${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^A-Za-z]|$)`, /^[A-Z+-]+$/.test(term) ? '' : 'i');
101
+ if (re.test(s[k] || '')) hits.push(`${s.name}.${k}: "${term}"`);
102
+ }
103
+ }
104
+ }
105
+ assert.deepEqual(hits, []);
106
+ });
107
+
108
+ await test('a skill with a paid step says so in its cost line', () => {
109
+ // The ones whose run spends money: every hand-in pays for a quality review,
110
+ // and image generation is billed per image. Named, so dropping the word from
111
+ // one of them is caught rather than trusted.
112
+ const paid = ['builder-ship', 'fix-task', 'builder-sequence', 'grade-recover', 'tweak', 'otb-tile-generate', 'imagegen-frontend-web', 'imagegen-frontend-mobile'];
113
+ const all = everySkill();
114
+ const quiet = paid.filter((n) => !/\bpaid\b|costs money/i.test((all.get(n) || {}).cost || ''));
115
+ assert.deepEqual(quiet, []);
116
+ });
117
+
76
118
  await test('explainSkill splits a model-facing description into what it does, when, and what it costs', () => {
77
119
  const e = skills.explainSkill({
78
120
  name: 'goal-review', source: 'core',
@@ -262,6 +304,15 @@ try {
262
304
  store.speciality = saved;
263
305
  });
264
306
 
307
+ await test('GET /specialities/installed-skills lists every installed skill, explained, for the owner\'s picker (task 1004404)', async () => {
308
+ const r = await call('GET', '/specialities/installed-skills');
309
+ assert.equal(r.status, 200, `not captured by /specialities/:id: ${JSON.stringify(r.body)}`);
310
+ assert.deepEqual(r.body.skills.map((s) => s.name), ['costing', 'recall', 'status'], 'every installed skill, by name — the choices are what the instance has');
311
+ const recall = r.body.skills.find((s) => s.name === 'recall');
312
+ assert.equal(recall.does, 'Search what the project knows. Read-only.', 'the same explanation the walkthrough shows');
313
+ assert.match(recall.cost, /^Read-only/);
314
+ });
315
+
265
316
  await test('PUT /specialities/:id/skills needs an active adoption', async () => {
266
317
  store.adoption = null;
267
318
  const r = await call('PUT', '/specialities/9/skills', { enabled_skills: ['recall'] });