@bongos/core 1.19.486 → 1.19.488
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.
- package/.bongos-core.json +39 -34
- package/docs/api/openapi.json +1 -1
- package/docs/copy-inventory.md +55 -55
- package/docs/copy-registry.json +63 -63
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/dom-utils.js +23 -1
- package/modules/hall-ui/public/hall-render.js +7 -8
- package/modules/hall-ui/public/settings.js +8 -7
- package/modules/lifecycle/db-claim-reads.js +16 -3
- package/modules/lifecycle/help-requests.js +3 -3
- package/modules/lifecycle/migrations/lifecycle_006_help_requests_ui_craft.sql +38 -0
- package/modules/lifecycle/ship-card.js +4 -1
- package/modules/onboarding/newcomer-restock.js +53 -22
- package/modules/ui-design/module.json +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/newcomer-floor.config.json +2 -1
- package/scripts/gds/newcomer-floor.js +7 -1
- package/scripts/gds/newcomer-floor.templates.json +43 -1
- package/scripts/gds/ship-grade-steps.js +23 -1
- package/scripts/gds/ship.js +4 -0
- package/src/bongos/db.js +3 -3
- package/src/bongos/routes/me.js +2 -1
- package/src/module-api.js +1 -1
- package/tests/grader_regrade_delta.mjs +57 -0
- package/tests/help_requests.mjs +21 -2
- package/tests/module_contributions.mjs +67 -0
- package/tests/modules.mjs +19 -1
- package/tests/newcomer_restock.mjs +76 -0
|
@@ -20,10 +20,10 @@ const { pool } = require('../../src/module-api');
|
|
|
20
20
|
// spells this same craft 'engineer', not 'builder'. THIS FUNCTION IS THE ONE PLACE
|
|
21
21
|
// that knows the mapping — the matching surface (C02) must consume it rather than
|
|
22
22
|
// re-deriving, or the two spellings drift and a builder silently matches nothing.
|
|
23
|
-
const HELP_CRAFTS = Object.freeze(['artist', 'builder', 'ideator']);
|
|
24
|
-
const CRAFT_TO_DISCIPLINE = Object.freeze({ artist: 'artist', ideator: 'ideator', builder: 'engineer' });
|
|
23
|
+
const HELP_CRAFTS = Object.freeze(['artist', 'builder', 'ideator', 'ui']);
|
|
24
|
+
const CRAFT_TO_DISCIPLINE = Object.freeze({ artist: 'artist', ideator: 'ideator', builder: 'engineer', ui: 'ui' });
|
|
25
25
|
|
|
26
|
-
const DISCIPLINE_TO_CRAFT = Object.freeze({ artist: 'artist', ideator: 'ideator', engineer: 'builder' });
|
|
26
|
+
const DISCIPLINE_TO_CRAFT = Object.freeze({ artist: 'artist', ideator: 'ideator', engineer: 'builder', ui: 'ui' });
|
|
27
27
|
|
|
28
28
|
function helpCrafts() { return [...HELP_CRAFTS]; }
|
|
29
29
|
function disciplineForCraft(craft) { return CRAFT_TO_DISCIPLINE[craft] || null; }
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
-- lifecycle_006_help_requests_ui_craft.sql — the `ui` craft becomes addressable in
|
|
2
|
+
-- a help request (task 1003487, the routing half of goal 1000025's UI-reachability
|
|
3
|
+
-- pair).
|
|
4
|
+
--
|
|
5
|
+
-- WHY THIS MIGRATION EXISTS AT ALL. lifecycle_004 fixed the craft vocabulary to
|
|
6
|
+
-- ADR 0174's three (artist, builder, ideator) in a CHECK. `ui` became a real
|
|
7
|
+
-- discipline later (migration 176 widened `builders.preferred_disciplines`, and
|
|
8
|
+
-- discipline-modes.json routes a ui task to /design), and task 1003487 makes it
|
|
9
|
+
-- selectable everywhere a builder chooses work. Every OTHER surface inherits that
|
|
10
|
+
-- automatically because it validates against enabledDisciplines() at run time —
|
|
11
|
+
-- this one does not, because a CHECK is compiled into the table. Without this the
|
|
12
|
+
-- app would accept a help request addressed to `ui` (helpCrafts() now offers it)
|
|
13
|
+
-- and Postgres would reject the INSERT, which is the worst shape of all: a
|
|
14
|
+
-- constraint violation surfacing as a 500 on a feature that reads as supported.
|
|
15
|
+
--
|
|
16
|
+
-- THE VOCABULARY SEAM IS UNCHANGED. `builders.preferred_disciplines` spells the
|
|
17
|
+
-- engineering craft 'engineer' while a help request spells it 'builder'; `ui` is
|
|
18
|
+
-- spelled the same on both sides, so it simply joins the list. help-requests.js
|
|
19
|
+
-- stays the single place that knows the mapping (CRAFT_TO_DISCIPLINE /
|
|
20
|
+
-- DISCIPLINE_TO_CRAFT), and craftMapSql() generates its SQL CASE from that same
|
|
21
|
+
-- object — so nothing here needs to restate it.
|
|
22
|
+
--
|
|
23
|
+
-- SAFE TO RE-RUN. Dropping by name and re-adding is idempotent in effect; existing
|
|
24
|
+
-- rows all carry one of the three older values (or NULL) and satisfy the wider
|
|
25
|
+
-- CHECK, so the ADD VALIDATE cannot fail on live data. No data is rewritten.
|
|
26
|
+
|
|
27
|
+
ALTER TABLE lifecycle_help_requests
|
|
28
|
+
DROP CONSTRAINT IF EXISTS lifecycle_help_requests_craft_chk;
|
|
29
|
+
|
|
30
|
+
ALTER TABLE lifecycle_help_requests
|
|
31
|
+
ADD CONSTRAINT lifecycle_help_requests_craft_chk
|
|
32
|
+
CHECK (needs_craft IS NULL OR needs_craft IN ('artist', 'builder', 'ideator', 'ui'));
|
|
33
|
+
|
|
34
|
+
COMMENT ON COLUMN lifecycle_help_requests.needs_craft IS
|
|
35
|
+
'The craft this request is addressed to: ADR 0174''s three (artist, builder, ideator) plus ui, which the ui-design module contributes as a discipline (task 1003487). helpCrafts() in modules/lifecycle/help-requests.js is the single place that knows this vocabulary and its spelling seam with builders.preferred_disciplines.';
|
|
36
|
+
|
|
37
|
+
INSERT INTO schema_migrations (version) VALUES ('lifecycle_006_help_requests_ui_craft')
|
|
38
|
+
ON CONFLICT (version) DO NOTHING;
|
|
@@ -159,7 +159,10 @@ function fmtMins(n) {
|
|
|
159
159
|
return m ? `~${h}h${m}m` : `~${h}h`;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
// Mirrors scripts/gds/discipline-modes.json, which is what claim.js prints. 'ui'
|
|
163
|
+
// was missing here, so a UI task's card sent the builder to /dev while the claim
|
|
164
|
+
// had just sent them to /design (task 1003487).
|
|
165
|
+
const DISCIPLINE_SKILL = { engineer: 'dev', artist: 'paint', ideator: 'ideate', ui: 'design' };
|
|
163
166
|
|
|
164
167
|
// The next-action per stage: { kind:'prompt', label, cmd } or { kind:'link', label, url }.
|
|
165
168
|
function nextActionFor(stage, { taskId, discipline, sandboxUrl } = {}) {
|
|
@@ -32,22 +32,48 @@ const TEMPLATES_PATH = path.join(REPO_ROOT, 'scripts/gds/newcomer-floor.template
|
|
|
32
32
|
const CONFIG_PATH = path.join(REPO_ROOT, 'scripts/gds/newcomer-floor.config.json');
|
|
33
33
|
const SOURCE_TAG = 'newcomer-template';
|
|
34
34
|
const HARD_MAX_MINUTES = 60;
|
|
35
|
-
|
|
35
|
+
// The crafts THIS instance offers, read through the published doorway (the one
|
|
36
|
+
// core file a module may require, ADR 0083). This was a hardcoded Set that did not
|
|
37
|
+
// track enabledDisciplines(), so it silently diverged the moment a module
|
|
38
|
+
// contributed a craft: the picker could offer it, a builder could save it, and
|
|
39
|
+
// restock would still never stamp its chores — a newcomer whose only craft was that
|
|
40
|
+
// one saw an empty board with nothing to self-heal it (task 1003487).
|
|
41
|
+
function validDisciplines() {
|
|
42
|
+
const { enabledDisciplines } = require('../../src/module-api');
|
|
43
|
+
return new Set(enabledDisciplines());
|
|
44
|
+
}
|
|
36
45
|
|
|
37
46
|
// Each discipline -> ARRAY of chore templates. A bare object (the pre-1488 single-
|
|
38
47
|
// chore shape) is tolerated and wrapped, so an older/other instance's templates.json
|
|
39
48
|
// still loads. Only entries with the required fields survive.
|
|
49
|
+
//
|
|
50
|
+
// Keyed by whatever the FILE declares (a leading-underscore key is metadata, not a
|
|
51
|
+
// craft) and deliberately NOT narrowed to what this instance offers: this is the
|
|
52
|
+
// catalogue of chores that EXIST, and choosing which of them to restock is
|
|
53
|
+
// restockAll's job. Narrowing here would drop a disabled craft's chores from the
|
|
54
|
+
// catalogue outright, which is a different question from "should we stamp them".
|
|
40
55
|
function loadTemplates() {
|
|
41
56
|
const raw = JSON.parse(fs.readFileSync(TEMPLATES_PATH, 'utf8'));
|
|
42
57
|
const out = {};
|
|
43
|
-
for (const d of
|
|
44
|
-
if (
|
|
45
|
-
const list = Array.isArray(
|
|
58
|
+
for (const [d, v] of Object.entries(raw)) {
|
|
59
|
+
if (d.startsWith('_') || !v) continue;
|
|
60
|
+
const list = Array.isArray(v) ? v : [v];
|
|
46
61
|
out[d] = list.filter((t) => t && t.template_id && t.version_id && t.title);
|
|
47
62
|
}
|
|
48
63
|
return out;
|
|
49
64
|
}
|
|
50
65
|
|
|
66
|
+
// The crafts that HAVE chores defined — the guard for "can this discipline be
|
|
67
|
+
// restocked at all", as distinct from validDisciplines() ("does this instance
|
|
68
|
+
// offer it"), which is what decides whether we actually stamp.
|
|
69
|
+
//
|
|
70
|
+
// Takes an ALREADY-LOADED map: every hot path reads the templates once and passes
|
|
71
|
+
// it here and to restockDiscipline, so a single restock does one readFileSync of
|
|
72
|
+
// newcomer-floor.templates.json rather than one per helper per call.
|
|
73
|
+
function templateDisciplines(byDiscipline = loadTemplates()) {
|
|
74
|
+
return new Set(Object.keys(byDiscipline));
|
|
75
|
+
}
|
|
76
|
+
|
|
51
77
|
// Per-discipline DEFAULT floor from config (fallback when a template omits its own).
|
|
52
78
|
function loadFloors() {
|
|
53
79
|
try {
|
|
@@ -150,9 +176,9 @@ async function restockTemplate(discipline, template, floors) {
|
|
|
150
176
|
|
|
151
177
|
// Restock EVERY chore of a discipline to its floor. Returns an ARRAY of per-template
|
|
152
178
|
// results (so a discipline with several chores tops each one up independently).
|
|
153
|
-
async function restockDiscipline(discipline) {
|
|
154
|
-
|
|
155
|
-
|
|
179
|
+
async function restockDiscipline(discipline, byDiscipline = loadTemplates()) {
|
|
180
|
+
const templates = byDiscipline[discipline];
|
|
181
|
+
if (!templates) return [{ discipline, skipped: 'invalid discipline' }];
|
|
156
182
|
if (!templates.length) return [{ discipline, skipped: 'no usable template' }];
|
|
157
183
|
const floors = loadFloors();
|
|
158
184
|
const out = [];
|
|
@@ -166,31 +192,35 @@ async function restockDiscipline(discipline) {
|
|
|
166
192
|
// be matched to a known template. Returns an array of per-template results, or
|
|
167
193
|
// [{ skipped:true }] for any non-chore task (callers ignore the value).
|
|
168
194
|
async function restockForClaim(task) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
const
|
|
195
|
+
// The cheap rejections first: a non-chore task is the common case and must not
|
|
196
|
+
// pay a file read at all.
|
|
197
|
+
if (!task || task.source !== SOURCE_TAG) return [{ skipped: true }];
|
|
198
|
+
const byDiscipline = loadTemplates();
|
|
199
|
+
const templates = byDiscipline[task.discipline];
|
|
200
|
+
if (!templates || !templates.length) return [{ skipped: true }];
|
|
173
201
|
const tid = templateIdFromRef(task.source_ref, task.discipline);
|
|
174
202
|
const match = tid ? templates.find((t) => t.template_id === tid) : null;
|
|
175
203
|
if (match) return [await restockTemplate(task.discipline, match, loadFloors())];
|
|
176
204
|
// ref didn't resolve to a known template — top the whole discipline up (safe).
|
|
177
|
-
|
|
205
|
+
// Hand the map down: restockDiscipline's default param would otherwise re-read
|
|
206
|
+
// and re-parse the file we are already holding.
|
|
207
|
+
return restockDiscipline(task.discipline, byDiscipline);
|
|
178
208
|
}
|
|
179
209
|
|
|
180
210
|
// Seed / backstop: bring every chore THIS INSTANCE offers to floor in one shot.
|
|
181
|
-
// R59 (task 1198): the restocked set is the
|
|
182
|
-
//
|
|
183
|
-
//
|
|
211
|
+
// R59 (task 1198): the restocked set is exactly the crafts this instance enables —
|
|
212
|
+
// a vanilla instance with the art pipeline off offers no 'artist', one with
|
|
213
|
+
// ui-design on offers 'ui'. A craft with no template in templates.json is simply
|
|
214
|
+
// skipped by restockDiscipline. Returns a FLAT array of per-template results.
|
|
184
215
|
async function restockAll() {
|
|
185
216
|
// enabledDisciplines reaches through the published doorway (the one core file a
|
|
186
217
|
// module may require, ADR 0083) — never a direct require of src/modules.js.
|
|
187
|
-
const
|
|
188
|
-
const
|
|
218
|
+
const offered = validDisciplines();
|
|
219
|
+
const byDiscipline = loadTemplates(); // one read for the whole sweep, not one per craft
|
|
189
220
|
const out = [];
|
|
190
|
-
for (const d of
|
|
191
|
-
if (offered.has(d))
|
|
192
|
-
|
|
193
|
-
}
|
|
221
|
+
for (const d of templateDisciplines(byDiscipline)) {
|
|
222
|
+
if (!offered.has(d)) continue;
|
|
223
|
+
for (const r of await restockDiscipline(d, byDiscipline)) out.push(r);
|
|
194
224
|
}
|
|
195
225
|
return out;
|
|
196
226
|
}
|
|
@@ -207,6 +237,7 @@ module.exports = {
|
|
|
207
237
|
templateIdFromRef,
|
|
208
238
|
buildDescription,
|
|
209
239
|
SOURCE_TAG,
|
|
210
|
-
|
|
240
|
+
validDisciplines,
|
|
241
|
+
templateDisciplines,
|
|
211
242
|
HARD_MAX_MINUTES,
|
|
212
243
|
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"coreVersion": "^1.19.0",
|
|
6
6
|
"default": true,
|
|
7
7
|
"maintenance": { "status": "core-maintained" },
|
|
8
|
-
"contributes": { "skills": ["design", "design-sync", "figma-design-sync", "design-taste-frontend", "design-taste-frontend-v1", "high-end-visual-design", "redesign-existing-projects", "impeccable", "style", "minimalist-ui", "industrial-brutalist-ui", "gpt-taste", "stitch-design-taste", "image-to-code", "imagegen-frontend-web", "imagegen-frontend-mobile", "brandkit"] },
|
|
8
|
+
"contributes": { "skills": ["design", "design-sync", "figma-design-sync", "design-taste-frontend", "design-taste-frontend-v1", "high-end-visual-design", "redesign-existing-projects", "impeccable", "style", "minimalist-ui", "industrial-brutalist-ui", "gpt-taste", "stitch-design-taste", "image-to-code", "imagegen-frontend-web", "imagegen-frontend-mobile", "brandkit"], "disciplines": ["ui"] },
|
|
9
9
|
"provides": [],
|
|
10
10
|
"consumes": []
|
|
11
11
|
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.488",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.488",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.488",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -58,7 +58,13 @@ const { cliClient, requireSession } = require('./cli-lib');
|
|
|
58
58
|
const DEFAULT_CONFIG_PATH = path.join(__dirname, 'newcomer-floor.config.json');
|
|
59
59
|
const SOURCE_TAG = 'newcomer-template';
|
|
60
60
|
const HARD_MAX_MINUTES = 60; // newcomer scope ceiling, never exceeded (task 614)
|
|
61
|
-
|
|
61
|
+
// The crafts this seeder will stamp chores for. Deliberately a literal and NOT
|
|
62
|
+
// enabledDisciplines(): this CLI seeds a REMOTE instance, whose enabled modules are
|
|
63
|
+
// not this checkout's, so gating on the local module set would seed the wrong shelf.
|
|
64
|
+
// It is the craft VOCABULARY, which is why 'unclassified' is absent. Keep in step
|
|
65
|
+
// with the keys in newcomer-floor.templates.json ('ui' added by task 1003487); the
|
|
66
|
+
// server-side restock reads the live offered set instead (newcomer-restock.js).
|
|
67
|
+
const VALID_DISCIPLINES = new Set(['engineer', 'artist', 'ideator', 'ui']);
|
|
62
68
|
// Every status a copy's source_ref could occupy — scanned so `seq` is globally
|
|
63
69
|
// monotonic and never recycles a ref already used by a shipped/abandoned copy.
|
|
64
70
|
const ALL_STATUSES = ['backlog', 'ready', 'active', 'completed', 'confirmed', 'shipped', 'blocked', 'abandoned'];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_doc": "The evergreen newcomer chores, grouped BY DISCIPLINE. Each discipline maps to a LIST of chore templates (was a single object before task 1488 — a discipline can now offer several first-builder chores so newcomers are not funnelled into one). The deterministic replenisher (scripts/gds/newcomer-floor.js) and the on-claim restock (modules/onboarding/newcomer-restock.js) stamp disposable claimable COPIES from each template; the TEMPLATE is never claimed and never ships. Each copy's source_ref is tmpl:<discipline>:<template_id>:<seq> (monotonic), so identical-title copies never collide with the dedup. Per-template `floor` sets that chore’s standing pool depth; if a template omits it, the discipline default in newcomer-floor.config.json applies. credits_reward, est_minutes and floor all live here (per chore). version_id is per-template so each chore lands on the correct track; it MUST point at an open (non-shipped) version — update it at version close, or the replenisher's open-version guard refuses to stamp and the floor silently stops refilling (task 1002958: the engineer pins still read the long-shipped GDS-V4 and artist+ideator read V2, a version this ladder never had, so NO discipline could restock). touches[] is intentionally empty so multiple same-discipline newcomers can claim copies concurrently; git-merge is the real collision detector. To ADD a chore: append an entry to the discipline's array. To CHANGE one: edit it here — no code change needed. Engineer offers a SAFE coding chore (add a unit test — touches only tests/, which auto-ships) and the docs-hygiene sweep (constrained to non-protected docs so a sub-Metic ship does not stall on the authority core).",
|
|
2
|
+
"_doc": "The evergreen newcomer chores, grouped BY DISCIPLINE. Each discipline maps to a LIST of chore templates (was a single object before task 1488 — a discipline can now offer several first-builder chores so newcomers are not funnelled into one). The deterministic replenisher (scripts/gds/newcomer-floor.js) and the on-claim restock (modules/onboarding/newcomer-restock.js) stamp disposable claimable COPIES from each template; the TEMPLATE is never claimed and never ships. Each copy's source_ref is tmpl:<discipline>:<template_id>:<seq> (monotonic), so identical-title copies never collide with the dedup. Per-template `floor` sets that chore’s standing pool depth; if a template omits it, the discipline default in newcomer-floor.config.json applies. credits_reward, est_minutes and floor all live here (per chore). version_id is per-template so each chore lands on the correct track; it MUST point at an open (non-shipped) version — update it at version close, or the replenisher's open-version guard refuses to stamp and the floor silently stops refilling (task 1002958: the engineer pins still read the long-shipped GDS-V4 and artist+ideator read V2, a version this ladder never had, so NO discipline could restock). touches[] is intentionally empty so multiple same-discipline newcomers can claim copies concurrently; git-merge is the real collision detector. To ADD a chore: append an entry to the discipline's array. To CHANGE one: edit it here — no code change needed. UI offers two browser-free chores (a token-drift fix measured by validate-design.js, and an accessible-name fix) so a newcomer whose only craft is ui is never starved for work on a machine with no Chrome/Edge for the kit — task 1003487. Engineer offers a SAFE coding chore (add a unit test — touches only tests/, which auto-ships) and the docs-hygiene sweep (constrained to non-protected docs so a sub-Metic ship does not stall on the authority core).",
|
|
3
3
|
"engineer": [
|
|
4
4
|
{
|
|
5
5
|
"template_id": "unit-test-pass",
|
|
@@ -82,5 +82,47 @@
|
|
|
82
82
|
"touches": [],
|
|
83
83
|
"floor": 2
|
|
84
84
|
}
|
|
85
|
+
],
|
|
86
|
+
"ui": [
|
|
87
|
+
{
|
|
88
|
+
"template_id": "ui-token-drift-pass",
|
|
89
|
+
"version_id": "BONGOS-V1",
|
|
90
|
+
"discipline": "ui",
|
|
91
|
+
"kind": "cleanup",
|
|
92
|
+
"est_minutes": 40,
|
|
93
|
+
"credits_reward": 15,
|
|
94
|
+
"floor": 2,
|
|
95
|
+
"title": "Token-drift pass: replace one hardcoded colour in a platform stylesheet with the branding token it should have used",
|
|
96
|
+
"description": "## What you're doing\n\nEvery colour on a platform surface must come from the instance's branding pack — **the Fifteen Rule**: thirteen colours plus two font stacks in `theme.ui`, reached as a `var()` or a `color-mix()` of them. The only legal literal is pure black or white with alpha, used as a scrim, mask or halo. Drift happens anyway: someone types `#3b82f6` in a hurry and the surface stops following the instance's world. Your first ship as a UI builder: find **one** real hardcoded colour in **one** stylesheet and replace it with the right token.\n\nIt is identical for every newcomer and **always valuable** — the sheets are large, drift is continuous, and every literal you convert is one more thing that follows the pack when an instance changes its look. It is also **safe**: you change one declaration in one CSS file, which is not a protected surface.\n\n## Learn the fifteen first (five minutes, and the whole job depends on it)\n\n1. Open `config/branding.json` (falling back to `config/branding.neutral.json`) and read `theme.ui` — those thirteen colours and two font stacks are your entire palette.\n2. Open the root `DESIGN.md` — it names the derived tiers and which token means what (surface, ink, accent, hairline, danger…). If it names a rule, it wins.\n3. Skim `docs/recipes/ui-look-before-you-ship.md` for how the floors are measured.\n\n## Find a real target\n\nGrep a platform stylesheet for a raw colour and pick ONE that is genuinely wrong:\n\n```\ngrep -rnE '#[0-9a-fA-F]{3,8}\\b|rgba?\\(' modules/hall-ui/public/ modules/status-ui/public/ --include=*.css\n```\n\nGood targets: a hex or `rgb()` that duplicates a token's value, or one that is close to a token but slightly off (the worst kind — it looks fine until the pack changes).\n\n**Not a target, leave these alone:** `#000` / `#fff` / `rgba(0,0,0,…)` / `rgba(255,255,255,…)` used as a scrim, shadow, mask or halo — those are explicitly legal. A colour inside a vendored or generated file. Anything under `config/` (changing the pack itself is the owner's decision and an ADR, never a first ship).\n\n## Make the change\n\nReplace the literal with the token that carries the same MEANING, not merely the nearest number — `var(--ui-accent)` because the thing is an accent, not because the hex was blue-ish. If no token means what the literal meant, you have found a design question rather than drift: pick a different target and, if you like, file the question with `node scripts/gds/capture.js`.\n\n**Both modes.** If the rule you touch has a dark twin, the twin changes too and stays byte-identical — `:root[data-mode=\"dark\"]` and `@media (prefers-color-scheme: dark) { :root:not([data-mode=\"light\"]) }`. Never fix one and leave the other.\n\n## Verify + ship\n\n- `node scripts/gds/validate-design.js --strict` — exits 0.\n- `node scripts/gds/run-unit-tests.js` — still green.\n- Say in the ship notes which literal you replaced, with which token, and why that token is the right MEANING.\n\nHave a browser (Edge or Chrome) on the machine? Then also run `node modules/ui-design/kit/render.js --page <the page>` and look at the before/after — the work is judged by looking at it. Not required for this chore.\n\n## Stay clear of\n\nDo not edit `config/branding.json`, `config/branding.neutral.json`, `config/design-tokens*.json`, or the root `DESIGN.md` — the world is the owner's, and changing it needs an ADR. Do not add a sixteenth token. Do not restyle the component while you are in there: one literal, one meaning, one commit. Touch no permission, pipeline, migration or infra file.",
|
|
97
|
+
"done_when": [
|
|
98
|
+
"Exactly one hardcoded colour literal in ONE non-protected stylesheet was replaced with a branding token (a var() or a color-mix() of theme.ui) — a real literal, not a legal black/white scrim, shadow, mask or halo, and not a cosmetic no-op.",
|
|
99
|
+
"The token chosen matches the MEANING of what was styled (the ship notes say which token and why), and no sixteenth token was invented.",
|
|
100
|
+
"If the rule had a dark twin, the twin was changed too and the two remain byte-identical.",
|
|
101
|
+
"`node scripts/gds/validate-design.js --strict` exits 0 and `node scripts/gds/run-unit-tests.js` is green.",
|
|
102
|
+
"config/branding*.json, config/design-tokens*.json and the root DESIGN.md were NOT modified, and no permission, pipeline, migration or infra path was touched.",
|
|
103
|
+
"The ship notes name the file, the literal removed and the token that replaced it."
|
|
104
|
+
],
|
|
105
|
+
"touches": []
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"template_id": "ui-alt-text-pass",
|
|
109
|
+
"version_id": "BONGOS-V1",
|
|
110
|
+
"discipline": "ui",
|
|
111
|
+
"kind": "cleanup",
|
|
112
|
+
"est_minutes": 35,
|
|
113
|
+
"credits_reward": 15,
|
|
114
|
+
"floor": 1,
|
|
115
|
+
"title": "Accessibility pass: give one unlabelled control or image on a platform page a real accessible name",
|
|
116
|
+
"description": "## What you're doing\n\nA control a screen reader cannot name is a control some people cannot use. Icon-only buttons are the usual offenders — a `<button>` whose entire content is an `<i class=\"ti ti-x\">` announces as \"button\" and nothing else. Your first ship as a UI builder: find **one** genuinely unlabelled control or image on a platform page and give it a real accessible name.\n\nIt is identical for every newcomer and **always valuable** — the hall and status surfaces carry many icon buttons, new ones arrive constantly, and each label you add is permanent.\n\n## Find a real target\n\nLook in `modules/hall-ui/public/*.html` and `*.js`, and `modules/status-ui/public/`:\n\n```\ngrep -rnE '<button[^>]*>\\s*<i class=\"ti' modules/hall-ui/public/ modules/status-ui/public/\ngrep -rn '<img' modules/hall-ui/public/ modules/status-ui/public/ | grep -v 'alt='\n```\n\nA target is genuine when it has **no** `aria-label`, **no** `title`, **no** visible text inside it, and **no** `aria-labelledby`. Check before you \"fix\" it — many already have one.\n\n## Make the change\n\n- Icon-only button → add `aria-label=\"<what it does>\"`. Name the ACTION, not the icon: `aria-label=\"Close the dialog\"`, never `aria-label=\"x icon\"`.\n- Decorative icon sitting beside text that already says it → the right fix is `aria-hidden=\"true\"`, so it is not announced twice.\n- `<img>` → a real `alt` describing what it conveys; `alt=\"\"` only when it is purely decorative.\n- Text built by JavaScript goes through the same `escapeHtml` the surrounding code uses — match the file's existing idiom.\n\nOne control. Do not restyle it, do not rename its class, do not refactor the component.\n\n## Verify + ship\n\n- `node scripts/gds/run-unit-tests.js` — green.\n- Read the label back to yourself with the screen off: does it say what pressing this does? If not, rewrite it.\n- The ship notes name the file, the control, and the label you gave it.\n\n## Stay clear of\n\nDo not \"fix\" a control that already has an accessible name — that is a no-op ship. Do not touch the branding pack, `DESIGN.md`, or any permission, pipeline, migration or infra file.",
|
|
117
|
+
"done_when": [
|
|
118
|
+
"Exactly one control or image that genuinely had NO accessible name (no aria-label, no title, no visible text, no aria-labelledby, no alt) was given one, in one non-protected file.",
|
|
119
|
+
"The name describes the ACTION or the MEANING, not the icon ('Close the dialog', not 'x icon'); a purely decorative icon beside existing text was marked aria-hidden=\"true\" instead.",
|
|
120
|
+
"Nothing else about the control changed — no restyle, no class rename, no component refactor — and any generated text uses the file's existing escaping idiom.",
|
|
121
|
+
"`node scripts/gds/run-unit-tests.js` is green.",
|
|
122
|
+
"The branding pack, DESIGN.md, and every permission/pipeline/migration/infra path were untouched.",
|
|
123
|
+
"The ship notes name the file, the control and the accessible name added."
|
|
124
|
+
],
|
|
125
|
+
"touches": []
|
|
126
|
+
}
|
|
85
127
|
]
|
|
86
128
|
}
|
|
@@ -94,10 +94,31 @@ async function logGraderPanelCost(taskId, gradeResult) {
|
|
|
94
94
|
// audit — the compact regrade block recorded into the new grade's signals
|
|
95
95
|
// so grade_attempts rounds read as remediation history (the
|
|
96
96
|
// task-1002661 reader consumes this).
|
|
97
|
+
// The prior round's findings, from EITHER grade shape (task 1003488).
|
|
98
|
+
//
|
|
99
|
+
// A grade exists in two shapes and this function is handed both. The in-memory
|
|
100
|
+
// result `validateAndScore`/`serverSideGuards` return carries `issues` at the top
|
|
101
|
+
// level; a grade READ BACK from the API does not, because `task_grades` has no
|
|
102
|
+
// `issues` column and `db-grade.js` deliberately writes them into the signals
|
|
103
|
+
// jsonb instead. `--regrade` passes the DB row (`ship-flow.js`: `tRes.data.grade`),
|
|
104
|
+
// so reading only the top-level key silently yielded [] on every re-grade — the
|
|
105
|
+
// panel was told it was on round N while being handed nothing about round N-1,
|
|
106
|
+
// and the "answered findings can only re-veto with NEW evidence" protection
|
|
107
|
+
// (task 1002662) cannot hold when the answered findings are invisible.
|
|
108
|
+
//
|
|
109
|
+
// Top level wins when present so an in-memory result keeps its exact behaviour;
|
|
110
|
+
// `signals.issues` is the stored fallback. Mirrors how the caller already reaches
|
|
111
|
+
// for `signals.graded_head_sha` three lines from where it passes this row in.
|
|
112
|
+
function priorFindings(storedGrade) {
|
|
113
|
+
if (Array.isArray(storedGrade.issues)) return storedGrade.issues;
|
|
114
|
+
const fromSignals = storedGrade.signals && storedGrade.signals.issues;
|
|
115
|
+
return Array.isArray(fromSignals) ? fromSignals : [];
|
|
116
|
+
}
|
|
117
|
+
|
|
97
118
|
const PRIOR_ROUND_TEXT_CAP = 4000;
|
|
98
119
|
function buildPriorRoundContext({ storedGrade, responseText = '', interimCommits = '', interimDiffStat = '' }) {
|
|
99
120
|
if (!storedGrade || typeof storedGrade !== 'object') return null;
|
|
100
|
-
const issues =
|
|
121
|
+
const issues = priorFindings(storedGrade);
|
|
101
122
|
const notes = typeof storedGrade.notes_md === 'string' ? storedGrade.notes_md.trim() : '';
|
|
102
123
|
if (issues.length === 0 && !notes) return null;
|
|
103
124
|
const verdictLine = `Prior verdict: ${storedGrade.passed ? 'PASS' : 'FAIL'} · score ${storedGrade.score ?? '?'}/10 · ${issues.length} finding(s)`;
|
|
@@ -436,6 +457,7 @@ function printCreditsAndAchievements({ postGradeStatus, credits, creditsRaw, cre
|
|
|
436
457
|
module.exports = {
|
|
437
458
|
buildGradePayload,
|
|
438
459
|
buildPriorRoundContext,
|
|
460
|
+
priorFindings,
|
|
439
461
|
computeGradeResult,
|
|
440
462
|
formatGradeOutcome,
|
|
441
463
|
gradeAndRecord,
|
package/scripts/gds/ship.js
CHANGED
|
@@ -145,6 +145,7 @@ const {
|
|
|
145
145
|
buildGradePayload,
|
|
146
146
|
buildPriorRoundContext,
|
|
147
147
|
computeGradeResult,
|
|
148
|
+
priorFindings,
|
|
148
149
|
formatGradeOutcome,
|
|
149
150
|
gradeAndRecord,
|
|
150
151
|
} = require('./ship-grade-steps.js');
|
|
@@ -195,6 +196,9 @@ module.exports = {
|
|
|
195
196
|
// task 1002662: delta-aware re-grade — the prior-round payload builder,
|
|
196
197
|
// exported pure so the panel/audit shapes are pinned without git/network.
|
|
197
198
|
buildPriorRoundContext,
|
|
199
|
+
// task 1003488: the two-shape finding reader behind it (a stored grade keeps
|
|
200
|
+
// its findings in signals.issues; an in-memory one at the top level).
|
|
201
|
+
priorFindings,
|
|
198
202
|
// #943: pure grade-outcome formatter — exported so the grader-unavailable vs
|
|
199
203
|
// FAIL display branch is unit-tested without capturing stdout.
|
|
200
204
|
formatGradeOutcome,
|
package/src/bongos/db.js
CHANGED
|
@@ -144,9 +144,9 @@ async function linkAnthropicAccount(builderId, { email, userId }) {
|
|
|
144
144
|
// Replace a builder's preferred_disciplines (migrations 027 + 086). Input is
|
|
145
145
|
// deduped, order-normalized, and validated against the disciplines THIS INSTANCE
|
|
146
146
|
// offers — core (engineer, ideator) plus any enabled feature module's disciplines
|
|
147
|
-
// (
|
|
148
|
-
// task 1198). The DB CHECK still enforces the full known enum
|
|
149
|
-
// (engineer|artist|ideator) as a backstop; this is the instance-aware
|
|
147
|
+
// (art-pipeline adds 'artist', ui-design adds 'ui'), resolved via src/modules.js
|
|
148
|
+
// (R59 / task 1198; task 1003487). The DB CHECK still enforces the full known enum
|
|
149
|
+
// (engineer|artist|ideator|ui, migration 176) as a backstop; this is the instance-aware
|
|
150
150
|
// friendly-error layer, so a vanilla instance with the art pipeline off rejects
|
|
151
151
|
// 'artist' instead of silently storing an unofferable discipline. Returns the
|
|
152
152
|
// updated builder row, or throws { code: 'invalid_discipline', bad } if any
|
package/src/bongos/routes/me.js
CHANGED
|
@@ -373,7 +373,8 @@ module.exports = function buildMeRouter() {
|
|
|
373
373
|
|
|
374
374
|
// Self-service edit of the authenticated builder's preferred disciplines
|
|
375
375
|
// (migrations 027 + 086 / V3.R80). Body: { preferred_disciplines: ["artist", ...] }.
|
|
376
|
-
// Valid elements are
|
|
376
|
+
// Valid elements are whatever enabledDisciplines() offers — engineer|ideator core
|
|
377
|
+
// plus any enabled module's (artist, ui); [] clears the preference (a
|
|
377
378
|
// multi-element set just means several lanes). No rank gate — a builder owns their own
|
|
378
379
|
// profile. 422 invalid_discipline if any element is out of range.
|
|
379
380
|
// rank: any-builder — self-service profile edit; documented "no rank gate" in the
|
package/src/module-api.js
CHANGED
|
@@ -45,7 +45,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
45
45
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
46
46
|
// the entry to that file. Look for a version's history there, not here.
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
|
-
const CORE_VERSION = '1.19.
|
|
48
|
+
const CORE_VERSION = '1.19.488'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
49
49
|
|
|
50
50
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
51
51
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -184,5 +184,62 @@ test('buildPriorRoundContext caps oversized notes/response/interim text', () =>
|
|
|
184
184
|
assert.ok(ctx.forPanel.interim_md.length < 5000, 'interim capped');
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
+
// ---- task 1003488: the prior round is read from a STORED grade, not just an
|
|
188
|
+
// ---- in-memory one. A grade exists in two shapes and this function is handed
|
|
189
|
+
// ---- both; reading only the top-level key made every --regrade run blind.
|
|
190
|
+
|
|
191
|
+
test('prior round: a STORED grade carries its findings under signals.issues and they still reach the panel', () => {
|
|
192
|
+
// exactly what ship-flow.js passes on --regrade: tRes.data.grade, a DB row.
|
|
193
|
+
// task_grades has no issues column, so db-grade.js writes them into signals.
|
|
194
|
+
const dbRow = {
|
|
195
|
+
passed: false, score: 5, notes_md: '',
|
|
196
|
+
signals: {
|
|
197
|
+
issue_count: 2,
|
|
198
|
+
issues: [
|
|
199
|
+
{ severity: 'major', worker: 'efficiency', file: 'a.js', summary: 'redundant sync reads' },
|
|
200
|
+
{ severity: 'nit', worker: 'quality', file: 'b.js', summary: 'label drift' },
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
const ctx = ship.buildPriorRoundContext({ storedGrade: dbRow });
|
|
205
|
+
assert.ok(ctx, 'a stored grade with findings is delta-aware context, not null');
|
|
206
|
+
assert.equal(ctx.audit.prior_findings_count, 2, 'the count is the stored findings, not 0');
|
|
207
|
+
assert.match(ctx.forPanel.findings_md, /2 finding\(s\)/);
|
|
208
|
+
assert.match(ctx.forPanel.findings_md, /redundant sync reads/, 'the finding text rides to the panel');
|
|
209
|
+
assert.match(ctx.forPanel.findings_md, /\[major\] efficiency · a\.js/, 'severity, worker and file survive');
|
|
210
|
+
assert.ok(!/no per-finding rows/.test(ctx.forPanel.findings_md),
|
|
211
|
+
'it must not fall through to the "no rows" placeholder');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('prior round: an IN-MEMORY grade keeps its exact top-level behaviour', () => {
|
|
215
|
+
const inMemory = {
|
|
216
|
+
passed: false, score: 5, notes_md: '',
|
|
217
|
+
issues: [{ severity: 'major', worker: 'quality', file: 'c.js', summary: 'top level wins' }],
|
|
218
|
+
signals: { issues: [{ severity: 'nit', worker: 'x', file: 'd.js', summary: 'must not be preferred' }] },
|
|
219
|
+
};
|
|
220
|
+
const ctx = ship.buildPriorRoundContext({ storedGrade: inMemory });
|
|
221
|
+
assert.equal(ctx.audit.prior_findings_count, 1);
|
|
222
|
+
assert.match(ctx.forPanel.findings_md, /top level wins/);
|
|
223
|
+
assert.ok(!/must not be preferred/.test(ctx.forPanel.findings_md),
|
|
224
|
+
'top-level issues take precedence over signals.issues');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('priorFindings: reads either shape, and never throws on a malformed one', () => {
|
|
228
|
+
const f = ship.priorFindings;
|
|
229
|
+
assert.deepEqual(f({ issues: [{ severity: 'nit' }] }).length, 1, 'top level');
|
|
230
|
+
assert.deepEqual(f({ signals: { issues: [{ severity: 'nit' }, { severity: 'major' }] } }).length, 2, 'stored');
|
|
231
|
+
for (const bad of [{}, { issues: null }, { signals: null }, { signals: {} },
|
|
232
|
+
{ signals: { issues: 'nope' } }, { issues: 'nope', signals: { issues: [{ severity: 'nit' }] } }]) {
|
|
233
|
+
const out = f(bad);
|
|
234
|
+
assert.ok(Array.isArray(out), `array for ${JSON.stringify(bad)}`);
|
|
235
|
+
}
|
|
236
|
+
// a non-array top level must fall through to the stored shape, not swallow it
|
|
237
|
+
assert.equal(f({ issues: 'nope', signals: { issues: [{ severity: 'nit' }] } }).length, 1);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('prior round: a stored grade with NO findings anywhere is still null (unchanged)', () => {
|
|
241
|
+
assert.equal(ship.buildPriorRoundContext({ storedGrade: { passed: true, notes_md: '', signals: {} } }), null);
|
|
242
|
+
});
|
|
243
|
+
|
|
187
244
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
188
245
|
process.exit(failed === 0 ? 0 : 1);
|
package/tests/help_requests.mjs
CHANGED
|
@@ -42,8 +42,27 @@ test('an unknown craft is refused', () => {
|
|
|
42
42
|
assert.ok(reasons({ needs_craft: 'wizard', what_is_stuck: 'help' }).includes('unknown_craft'));
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
test('the crafts are ADR 0174\'s three', () => {
|
|
46
|
-
|
|
45
|
+
test('the crafts are ADR 0174\'s three, plus the ui craft the ui-design module contributes', () => {
|
|
46
|
+
// 'ui' joined the vocabulary with task 1003487, which made the discipline
|
|
47
|
+
// selectable everywhere a builder chooses work. The DB CHECK is widened in
|
|
48
|
+
// lockstep by modules/lifecycle/migrations/lifecycle_006_help_requests_ui_craft.sql.
|
|
49
|
+
assert.deepEqual(help.helpCrafts().sort(), ['artist', 'builder', 'ideator', 'ui']);
|
|
50
|
+
assert.equal(help.disciplineForCraft('ui'), 'ui', 'ui is spelled the same on both sides of the seam');
|
|
51
|
+
assert.equal(help.craftForDiscipline('ui'), 'ui');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('every craft the app offers is permitted by the help_requests CHECK', () => {
|
|
55
|
+
// The regression this pins: the vocabulary lives BOTH in JS (HELP_CRAFTS) and in
|
|
56
|
+
// a compiled-in CHECK, and widening one without the other is invisible until an
|
|
57
|
+
// INSERT fails in production as a 500 on a feature that reads as supported.
|
|
58
|
+
const sql = fs.readFileSync(
|
|
59
|
+
path.join(ROOT, 'modules', 'lifecycle', 'migrations', 'lifecycle_006_help_requests_ui_craft.sql'), 'utf8');
|
|
60
|
+
const m = sql.match(/needs_craft IN \(([^)]*)\)/);
|
|
61
|
+
assert.ok(m, 'the migration states the permitted craft list');
|
|
62
|
+
const allowed = m[1].split(',').map((x) => x.trim().replace(/^'|'$/g, ''));
|
|
63
|
+
for (const c of help.helpCrafts()) {
|
|
64
|
+
assert.ok(allowed.includes(c), `craft '${c}' is offered by helpCrafts() but the CHECK forbids it`);
|
|
65
|
+
}
|
|
47
66
|
});
|
|
48
67
|
|
|
49
68
|
test('THE VOCABULARY SEAM: builder maps to the discipline spelled engineer', () => {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import assert from 'node:assert/strict';
|
|
10
10
|
import { test } from 'node:test';
|
|
11
11
|
import { createRequire } from 'node:module';
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
12
13
|
import path from 'node:path';
|
|
13
14
|
import { fileURLToPath } from 'node:url';
|
|
14
15
|
|
|
@@ -153,6 +154,72 @@ test('settings-panel seam: ABSENT → empty; REGISTERED → present, sorted by o
|
|
|
153
154
|
// third-party widget can't blank the hall — so these assert the RETURN VALUE and
|
|
154
155
|
// that the widget never reached the registry.
|
|
155
156
|
|
|
157
|
+
// ---- browser twin: the craft picker's options (task 1003487) ----------------
|
|
158
|
+
// Both hall pickers (the onboarding pick_disciplines step and the Settings craft
|
|
159
|
+
// chips) build their options from OTB.disciplines(), so this IS the rendered
|
|
160
|
+
// option list. It used to be a hardcoded triple in each file, which is how a
|
|
161
|
+
// module-contributed craft could be offered by the server and appear in neither.
|
|
162
|
+
|
|
163
|
+
test('craft picker options: OTB.disciplines() mirrors the server list, and falls back to the core two', () => {
|
|
164
|
+
global.window = {};
|
|
165
|
+
// the IIFE assigns window.OTB once at load — evict it for a fresh binding
|
|
166
|
+
const domUtils = path.join(ROOT, 'modules', 'hall-ui', 'public', 'dom-utils.js');
|
|
167
|
+
delete require.cache[require.resolve(domUtils)];
|
|
168
|
+
require(domUtils);
|
|
169
|
+
const OTB = global.window.OTB;
|
|
170
|
+
|
|
171
|
+
// the server's list wins, in the server's order
|
|
172
|
+
global.window.__MODULES__ = { enabled: {}, disciplines: ['engineer', 'ideator', 'ui'] };
|
|
173
|
+
assert.deepEqual(OTB.disciplines(), ['engineer', 'ideator', 'ui']);
|
|
174
|
+
// a module-contributed craft is offered without any per-craft moduleOn() branch
|
|
175
|
+
global.window.__MODULES__ = { enabled: { 'art-pipeline': true }, disciplines: ['engineer', 'ideator', 'artist', 'ui'] };
|
|
176
|
+
assert.ok(OTB.disciplines().includes('ui') && OTB.disciplines().includes('artist'));
|
|
177
|
+
// withdrawn with its module
|
|
178
|
+
global.window.__MODULES__ = { enabled: {}, disciplines: ['engineer', 'ideator'] };
|
|
179
|
+
assert.deepEqual(OTB.disciplines(), ['engineer', 'ideator']);
|
|
180
|
+
|
|
181
|
+
// an older server that sends no list degrades to the core two, never to an
|
|
182
|
+
// empty picker (which would strand a builder with no way to pick a craft)
|
|
183
|
+
for (const m of [undefined, {}, { enabled: {} }, { disciplines: [] }, { disciplines: 'nope' }]) {
|
|
184
|
+
global.window.__MODULES__ = m;
|
|
185
|
+
assert.deepEqual(OTB.disciplines(), ['engineer', 'ideator'], `fallback for ${JSON.stringify(m)}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// returns a COPY — a picker mutating its options cannot corrupt the global
|
|
189
|
+
global.window.__MODULES__ = { disciplines: ['engineer', 'ideator', 'ui'] };
|
|
190
|
+
OTB.disciplines().push('bogus');
|
|
191
|
+
assert.deepEqual(OTB.disciplines(), ['engineer', 'ideator', 'ui']);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('craft picker options: disciplineLabel names an acronym correctly', () => {
|
|
195
|
+
global.window = {};
|
|
196
|
+
// the IIFE assigns window.OTB once at load — evict it for a fresh binding
|
|
197
|
+
const domUtils = path.join(ROOT, 'modules', 'hall-ui', 'public', 'dom-utils.js');
|
|
198
|
+
delete require.cache[require.resolve(domUtils)];
|
|
199
|
+
require(domUtils);
|
|
200
|
+
const OTB = global.window.OTB;
|
|
201
|
+
assert.equal(OTB.disciplineLabel('ui'), 'UI', 'not "Ui"');
|
|
202
|
+
assert.equal(OTB.disciplineLabel('engineer'), 'Engineer');
|
|
203
|
+
assert.equal(OTB.disciplineLabel('artist'), 'Artist');
|
|
204
|
+
assert.equal(OTB.disciplineLabel('ideator'), 'Ideator');
|
|
205
|
+
// a craft a module adds later needs no new entry here
|
|
206
|
+
assert.equal(OTB.disciplineLabel('archivist'), 'Archivist');
|
|
207
|
+
for (const bad of ['', null, undefined]) assert.equal(OTB.disciplineLabel(bad), '');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('craft picker options: neither picker hardcodes the craft list any more', () => {
|
|
211
|
+
// A regression pin with teeth: the bug was two independent literal triples that
|
|
212
|
+
// silently dropped a module-contributed craft. Whichever picker is edited next
|
|
213
|
+
// must keep reading the server list.
|
|
214
|
+
const read = (p) => readFileSync(path.join(ROOT, p), 'utf8');
|
|
215
|
+
for (const p of ['modules/hall-ui/public/settings.js', 'modules/hall-ui/public/hall-render.js']) {
|
|
216
|
+
const src = read(p);
|
|
217
|
+
assert.ok(/OTB\.disciplines\(\)/.test(src), `${p} builds its options from OTB.disciplines()`);
|
|
218
|
+
assert.ok(!/\['engineer',\s*'artist',\s*'ideator'\]/.test(src), `${p} no longer hardcodes the craft triple`);
|
|
219
|
+
assert.ok(!/discBtn\('engineer'/.test(src), `${p} no longer names a craft literally`);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
156
223
|
test('hall-widget registry: placement rules reject bad registrations, accept good ones', () => {
|
|
157
224
|
global.window = {};
|
|
158
225
|
// The settings-panel test above already required dom-utils.js, and the IIFE
|