@bongos/core 1.19.871 → 1.19.872
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 +163 -93
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/CONTRIBUTING.md +1 -1
- package/README.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +10 -0
- package/docs/api/openapi.json +61 -3
- package/docs/api-reference.md +7 -1
- package/docs/architecture.md +7 -7
- package/docs/branding-contract.md +1 -1
- package/docs/copy-inventory.md +190 -189
- package/docs/copy-registry.json +264 -255
- package/docs/design/hall-direction-v2.md +2 -2
- package/docs/module-api-changelog.md +2 -0
- package/docs/onboarding/diagrams/README.md +4 -4
- package/docs/onboarding/drachmae-and-karma.md +2 -2
- package/docs/onboarding/primer.md +1 -1
- package/docs/recipes/README.md +1 -1
- package/docs/recipes/bongos-cli-release.md +4 -4
- package/docs/recipes/core-release-pipeline.md +3 -3
- package/docs/recipes/instance-deploy.md +2 -2
- package/docs/recipes/ops-gotchas.md +2 -2
- package/docs/recipes/search-index-ingestion.md +2 -2
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/modules/autonomy/gauge.js +218 -0
- package/modules/dev-box/app/README.md +1 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
- package/modules/hall-ui/public/blockers.js +138 -34
- package/modules/hall-ui/public/builders.js +2 -1
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/collab.js +6 -3
- package/modules/hall-ui/public/dom-utils.js +46 -5
- package/modules/hall-ui/public/goals-page.js +7 -3
- package/modules/hall-ui/public/goals-render.js +4 -3
- package/modules/hall-ui/public/hall-render.js +1 -1
- package/modules/hall-ui/public/people.js +3 -3
- package/modules/hall-ui/public/profile-charts.js +5 -2
- package/modules/hall-ui/public/profile.html +3 -3
- package/modules/hall-ui/public/profile.js +6 -6
- package/modules/hall-ui/public/settings-specialities.js +2 -2
- package/modules/hall-ui/public/settings.html +4 -4
- package/modules/hall-ui/public/settings.js +2 -2
- package/modules/hall-ui/public/shell.js +70 -0
- package/modules/hall-ui/public/style.css +31 -0
- package/modules/hall-ui/public/work.js +12 -14
- package/modules/provisioning/module.json +3 -3
- package/modules/public-landing/public/terms.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/autobongos-loop.js +170 -0
- package/scripts/gds/autobongos-run.js +317 -0
- package/scripts/gds/autobongos-verify.js +377 -0
- package/scripts/gds/autonomy-gauge-statusline.js +124 -0
- package/scripts/gds/autonomy-gauge.js +84 -0
- package/scripts/gds/run-unit-tests.js +120 -2
- package/scripts/gds/sequence.js +3 -0
- package/scripts/gds/start.js +16 -10
- package/src/bongos/core-update.js +147 -0
- package/src/bongos/routes/core-update.js +49 -0
- package/src/bongos/routes.js +2 -0
- package/src/module-api.js +1 -1
- package/tests/account_privacy_flags.mjs +3 -1
- package/tests/autobongos_loop.mjs +446 -0
- package/tests/autobongos_verify.mjs +325 -0
- package/tests/autonomy_gauge.mjs +344 -0
- package/tests/blockers_hall.mjs +20 -6
- package/tests/blockers_kit_pager.mjs +260 -0
- package/tests/blockers_page_ui.mjs +5 -4
- package/tests/box_ship_permissions.mjs +30 -0
- package/tests/core_update_banner.mjs +308 -0
- package/tests/eviction_policy.mjs +22 -1
- package/tests/git_guard.mjs +68 -0
- package/tests/goal_criterion_confirm_ui.mjs +7 -0
- package/tests/goal_invite_consent.mjs +7 -0
- package/tests/goal_sort_staleness.mjs +6 -2
- package/tests/hall_board_world.mjs +4 -1
- package/tests/hall_builders_page_pass.mjs +7 -0
- package/tests/hall_landing_boot.mjs +7 -0
- package/tests/hall_profile_world.mjs +20 -2
- package/tests/hall_record_world.mjs +7 -0
- package/tests/hall_roster_world.mjs +2 -1
- package/tests/hall_studio_world.mjs +7 -0
- package/tests/hook_shell_tools.mjs +23 -1
- package/tests/idea_detail_page.mjs +7 -0
- package/tests/idea_spark_hall.mjs +7 -0
- package/tests/integration_parked_ratchet.mjs +79 -0
- package/tests/module_contributions.mjs +134 -8
- package/tests/people_directory_clip.mjs +7 -0
- package/tests/profile_activity.mjs +7 -0
- package/tests/profile_ui_cross_project.mjs +9 -1
- package/tests/provision_disconnect.mjs +7 -0
- package/tests/provision_restart.mjs +7 -0
- package/tests/provision_settings_apply.mjs +7 -0
- package/tests/sessions_kit_pager.mjs +7 -0
- package/tests/watch_applications_queue.mjs +7 -0
- package/tests/watch_roster_self_row.mjs +7 -0
- package/tests/watch_sealed_floor.mjs +7 -0
|
@@ -91,6 +91,13 @@ function render(idea, { template = TEMPLATE, templateFails = false, ideaStatus =
|
|
|
91
91
|
__BRANDING__: {}, __MODULES__: { enabled: {} },
|
|
92
92
|
BongosClient: { createClient: () => client },
|
|
93
93
|
OTB: {
|
|
94
|
+
// task 1003936 — the role axis vocabulary (dom-utils OTB.role*). PRESENT for
|
|
95
|
+
// the same reason categoryChip is: an undefined helper throws mid-render.
|
|
96
|
+
roleAxisLabel: () => 'Role',
|
|
97
|
+
roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
|
|
98
|
+
roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
|
|
99
|
+
roleIsUnset: (d) => !d || d === 'unclassified',
|
|
100
|
+
ROLE_UNSET: 'unclassified',
|
|
94
101
|
escapeHtml: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])),
|
|
95
102
|
$: (sel) => documentObj.querySelector(sel),
|
|
96
103
|
fmtDate: (iso) => String(iso).slice(0, 10),
|
|
@@ -126,6 +126,13 @@ function boot({
|
|
|
126
126
|
__BRANDING__: {}, __MODULES__: { enabled: {} },
|
|
127
127
|
BongosClient: { createClient: () => client },
|
|
128
128
|
OTB: {
|
|
129
|
+
// task 1003936 — the role axis vocabulary (dom-utils OTB.role*). PRESENT for
|
|
130
|
+
// the same reason categoryChip is: an undefined helper throws mid-render.
|
|
131
|
+
roleAxisLabel: () => 'Role',
|
|
132
|
+
roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
|
|
133
|
+
roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
|
|
134
|
+
roleIsUnset: (d) => !d || d === 'unclassified',
|
|
135
|
+
ROLE_UNSET: 'unclassified',
|
|
129
136
|
escapeHtml: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])),
|
|
130
137
|
$: (sel) => documentObj.querySelector(sel),
|
|
131
138
|
toast: (m) => toasts.push(m),
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// tests/integration_parked_ratchet.mjs — the DB-free half of INTEGRATION_PARKED's
|
|
2
|
+
// guarantees (task 1004034). Sibling of tests/known_failing_ratchet.mjs, which does
|
|
3
|
+
// the same duty for the DB-free lane's KNOWN_FAILING.
|
|
4
|
+
//
|
|
5
|
+
// THE SPLIT, AND WHY IT EXISTS. INTEGRATION_PARKED holds integration tests that fail
|
|
6
|
+
// and are excluded from the lane so it can gate the ones that pass. Its central
|
|
7
|
+
// promise — "a parked test that starts passing reds the lane until its entry goes" —
|
|
8
|
+
// can only be checked WITH a database, because a parked integration test run without
|
|
9
|
+
// one fails for want of the database and would report "still failing" whether or not
|
|
10
|
+
// anybody had fixed it. That half therefore lives in
|
|
11
|
+
// `run-unit-tests.js --integration-parked`, which the integration workflow runs as a
|
|
12
|
+
// blocking step.
|
|
13
|
+
//
|
|
14
|
+
// What is left is everything that does NOT need a database, and it is the half that
|
|
15
|
+
// rots quietest: an entry naming a file nobody kept, an entry for a test the lane was
|
|
16
|
+
// never going to run anyway, an entry double-listed in KNOWN_FAILING so two
|
|
17
|
+
// mechanisms disagree about who owns it, and the list quietly growing. None of those
|
|
18
|
+
// shows up as a red test anywhere else — the lane just silently gates less.
|
|
19
|
+
//
|
|
20
|
+
// DB-free, no network. Run: node tests/integration_parked_ratchet.mjs
|
|
21
|
+
|
|
22
|
+
import { strict as assert } from 'node:assert';
|
|
23
|
+
import { test } from 'node:test';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const TESTS_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const { INTEGRATION, INTEGRATION_PARKED, KNOWN_FAILING } = require('../scripts/gds/run-unit-tests.js');
|
|
32
|
+
|
|
33
|
+
// The ceiling. Twenty-three is what the integration lane's first ever run surfaced
|
|
34
|
+
// (task 1000862) — every one a test nothing had executed before, not a regression.
|
|
35
|
+
// Lowering it as they are fixed is the point; raising it means a NEW failure was
|
|
36
|
+
// parked instead of fixed, which is a deliberate, reviewable act.
|
|
37
|
+
//
|
|
38
|
+
// RAISED 23 → 24 by task 1004042, and this is that act with its reason attached:
|
|
39
|
+
// acclaim_feed_evidence_db is not a new failure, it is one of the original set that
|
|
40
|
+
// was held back because it happened to PASS in the CI run the list was read from.
|
|
41
|
+
// Two later readings — 2/2 locally on a clean database, and the first CI run after
|
|
42
|
+
// the parking landed — showed the pass was the anomaly. No new breakage was parked
|
|
43
|
+
// here; one straggler joined the cohort it always belonged to.
|
|
44
|
+
const MAX_PARKED = 24;
|
|
45
|
+
|
|
46
|
+
test('every parked entry names a test file that exists', () => {
|
|
47
|
+
const ghosts = [...INTEGRATION_PARKED]
|
|
48
|
+
.filter((name) => !fs.existsSync(path.join(TESTS_DIR, `${name}.mjs`)))
|
|
49
|
+
.sort();
|
|
50
|
+
assert.deepEqual(ghosts, [],
|
|
51
|
+
'these are parked but have no test file. An entry that outlives its test silently '
|
|
52
|
+
+ 'shrinks what the lane gates and can never be un-parked — delete the entry, or '
|
|
53
|
+
+ 'restore the file.');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('every parked entry is one the integration lane would otherwise run', () => {
|
|
57
|
+
// Parking a name the lane never selects gates nothing and hides nothing; it just
|
|
58
|
+
// makes the list look like it is doing more than it is.
|
|
59
|
+
const notInLane = [...INTEGRATION_PARKED].filter((name) => !INTEGRATION.has(name)).sort();
|
|
60
|
+
assert.deepEqual(notInLane, [],
|
|
61
|
+
'these are parked but are not in the INTEGRATION set, so the lane was never going '
|
|
62
|
+
+ 'to run them. Add them to INTEGRATION or drop the parked entry.');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('no test is parked in both INTEGRATION_PARKED and KNOWN_FAILING', () => {
|
|
66
|
+
// They are different mechanisms with different ratchets. A name in both means two
|
|
67
|
+
// lists claim ownership and only one of them will ever be the reason it is skipped,
|
|
68
|
+
// so removing it from one would look like a fix while the other kept it excluded.
|
|
69
|
+
const both = [...INTEGRATION_PARKED].filter((name) => KNOWN_FAILING.has(name)).sort();
|
|
70
|
+
assert.deepEqual(both, [],
|
|
71
|
+
'parked in two places at once — pick the mechanism that owns each test.');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('the parked count is ratcheted and cannot grow silently', () => {
|
|
75
|
+
const n = INTEGRATION_PARKED.size;
|
|
76
|
+
assert.ok(n <= MAX_PARKED,
|
|
77
|
+
`${n} parked integration tests exceeds the ratchet of ${MAX_PARKED}. Fix the new `
|
|
78
|
+
+ 'failure rather than parking it; raising MAX_PARKED is a deliberate act.');
|
|
79
|
+
});
|
|
@@ -9,7 +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
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
|
|
@@ -191,20 +191,75 @@ test('craft picker options: OTB.disciplines() mirrors the server list, and falls
|
|
|
191
191
|
assert.deepEqual(OTB.disciplines(), ['engineer', 'ideator', 'ui']);
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
-
test('craft picker options:
|
|
194
|
+
test('craft picker options: roleLabel names an acronym correctly', () => {
|
|
195
195
|
global.window = {};
|
|
196
196
|
// the IIFE assigns window.OTB once at load — evict it for a fresh binding
|
|
197
197
|
const domUtils = path.join(ROOT, 'modules', 'hall-ui', 'public', 'dom-utils.js');
|
|
198
198
|
delete require.cache[require.resolve(domUtils)];
|
|
199
199
|
require(domUtils);
|
|
200
200
|
const OTB = global.window.OTB;
|
|
201
|
-
assert.equal(OTB.
|
|
202
|
-
assert.equal(OTB.
|
|
203
|
-
assert.equal(OTB.
|
|
204
|
-
assert.equal(OTB.
|
|
201
|
+
assert.equal(OTB.roleLabel('ui'), 'UI', 'not "Ui"');
|
|
202
|
+
assert.equal(OTB.roleLabel('engineer'), 'Engineer');
|
|
203
|
+
assert.equal(OTB.roleLabel('artist'), 'Artist');
|
|
204
|
+
assert.equal(OTB.roleLabel('ideator'), 'Ideator');
|
|
205
205
|
// a craft a module adds later needs no new entry here
|
|
206
|
-
assert.equal(OTB.
|
|
207
|
-
for (const bad of ['', null, undefined]) assert.equal(OTB.
|
|
206
|
+
assert.equal(OTB.roleLabel('archivist'), 'Archivist');
|
|
207
|
+
for (const bad of ['', null, undefined]) assert.equal(OTB.roleLabel(bad), '');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ---- task 1003936: ONE word for the role axis, and it lives in one place -----
|
|
211
|
+
|
|
212
|
+
test('the role axis has exactly one user-facing word, and no surface types it', () => {
|
|
213
|
+
// The bug this pins: the hall shipped FOUR words for one axis — Discipline on the
|
|
214
|
+
// Tasks board, Discipline AND Craft on the Goals page 77 lines apart, Craft in the
|
|
215
|
+
// Directory and Settings, craft in Collab, Type/lane on the builder-start card.
|
|
216
|
+
// ADR 0174 Decision 5 settled the word as "role"; the owner confirmed it as the
|
|
217
|
+
// shipped label on 2026-09-18. A label repeated in six files drifts back, so the
|
|
218
|
+
// real invariant is not "the word is Role" but "no surface spells it itself".
|
|
219
|
+
global.window = {};
|
|
220
|
+
const domUtils = path.join(ROOT, 'modules', 'hall-ui', 'public', 'dom-utils.js');
|
|
221
|
+
delete require.cache[require.resolve(domUtils)];
|
|
222
|
+
require(domUtils);
|
|
223
|
+
const OTB = global.window.OTB;
|
|
224
|
+
assert.equal(OTB.roleAxisLabel(), 'Role');
|
|
225
|
+
|
|
226
|
+
const readSrc = (p) => readFileSync(path.join(ROOT, p), 'utf8');
|
|
227
|
+
const SURFACES = [
|
|
228
|
+
'modules/hall-ui/public/work.js',
|
|
229
|
+
'modules/hall-ui/public/goals-page.js',
|
|
230
|
+
'modules/hall-ui/public/people.js',
|
|
231
|
+
'modules/hall-ui/public/profile.js',
|
|
232
|
+
'modules/hall-ui/public/builders.js',
|
|
233
|
+
];
|
|
234
|
+
for (const p of SURFACES) {
|
|
235
|
+
const s = readSrc(p);
|
|
236
|
+
// No surface may carry a hand-typed label for the axis...
|
|
237
|
+
assert.ok(!/label: '(Craft|Discipline|Role)'/.test(s), p + ' types the axis label instead of reading roleAxisLabel()');
|
|
238
|
+
assert.ok(!/'Crafts'|'Disciplines'|'Roles'/.test(s), p + ' types the plural axis label');
|
|
239
|
+
// ...and every one of them must actually read the shared vocabulary.
|
|
240
|
+
assert.match(s, /OTB\.role(AxisLabel|Label|Options|IsUnset)\(/, p + ' does not read the shared role vocabulary');
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('an unset role is never shown to a person as the database default', () => {
|
|
245
|
+
// 'unclassified' is a column default, not a word anyone would say (docs/voice.md
|
|
246
|
+
// V2). It may remain the stored value and the filter VALUE; it may not be what a
|
|
247
|
+
// person reads.
|
|
248
|
+
global.window = {};
|
|
249
|
+
const domUtils = path.join(ROOT, 'modules', 'hall-ui', 'public', 'dom-utils.js');
|
|
250
|
+
delete require.cache[require.resolve(domUtils)];
|
|
251
|
+
require(domUtils);
|
|
252
|
+
const OTB = global.window.OTB;
|
|
253
|
+
assert.equal(OTB.ROLE_UNSET, 'unclassified', 'the stored value is unchanged');
|
|
254
|
+
assert.equal(OTB.roleLabel('unclassified'), 'No role yet');
|
|
255
|
+
assert.ok(OTB.roleIsUnset('unclassified') && OTB.roleIsUnset(null) && OTB.roleIsUnset(''));
|
|
256
|
+
assert.ok(!OTB.roleIsUnset('engineer'));
|
|
257
|
+
|
|
258
|
+
const readSrc = (p) => readFileSync(path.join(ROOT, p), 'utf8');
|
|
259
|
+
// The role-bearing render sites must not interpolate the raw default into markup.
|
|
260
|
+
for (const p of ['modules/hall-ui/public/work.js', 'modules/hall-ui/public/profile.js', 'modules/hall-ui/public/profile-charts.js']) {
|
|
261
|
+
assert.ok(!/t\.discipline \|\| 'unclassified'/.test(readSrc(p)), p + ' still renders the stored default');
|
|
262
|
+
}
|
|
208
263
|
});
|
|
209
264
|
|
|
210
265
|
test('craft picker options: neither picker hardcodes the craft list any more', () => {
|
|
@@ -268,3 +323,74 @@ test('hall-widget registry: placement rules reject bad registrations, accept goo
|
|
|
268
323
|
delete global.window;
|
|
269
324
|
}
|
|
270
325
|
});
|
|
326
|
+
|
|
327
|
+
// ---- task 1004039: the role axis on the PUBLIC LEGAL surface ----------------
|
|
328
|
+
|
|
329
|
+
test('the public landing pages spell the role axis one way, and Terms actually says it', () => {
|
|
330
|
+
// Task 1003936 unified the word to "Role" across the hall, the status dashboard and
|
|
331
|
+
// the builder-start card, but deliberately left Terms alone: it is a public legal
|
|
332
|
+
// surface, and rewording one is a different decision from renaming a filter label.
|
|
333
|
+
// That left a FIFTH spelling in the wild — "the disciplines you worked in" — on the
|
|
334
|
+
// one page whose whole job is to describe accurately what the platform publishes
|
|
335
|
+
// about a person.
|
|
336
|
+
//
|
|
337
|
+
// OWNER CONFIRMATION (the task's third done-when: confirm before shipping, because a
|
|
338
|
+
// legal surface is not a pure copy edit). Asked and answered 2026-09-19 — the owner
|
|
339
|
+
// was shown the current sentence, the proposed "the roles you worked in", and that
|
|
340
|
+
// the change is label-only with the published fields unchanged, and chose to ship it.
|
|
341
|
+
// Recorded here because a reviewer reads the diff, not the conversation.
|
|
342
|
+
//
|
|
343
|
+
// Scanned as RENDERED TEXT, not source lines. Task 1003936 burned four grading
|
|
344
|
+
// rounds on greps keyed to the shape the word was expected to have: a sentence split
|
|
345
|
+
// across three lines inside one <p> defeats a line-anchored HTML grep. Strip
|
|
346
|
+
// script/style/comments/tags, collapse whitespace, then scan the prose a reader sees.
|
|
347
|
+
const dir = path.join(ROOT, 'modules', 'public-landing', 'public');
|
|
348
|
+
const renderedText = (file) => readFileSync(path.join(dir, file), 'utf8')
|
|
349
|
+
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
|
350
|
+
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|
351
|
+
.replace(/<!--[\s\S]*?-->/g, ' ')
|
|
352
|
+
.replace(/<[^>]+>/g, ' ')
|
|
353
|
+
.replace(/\s+/g, ' ');
|
|
354
|
+
|
|
355
|
+
const pages = readdirSync(dir).filter((f) => f.endsWith('.html'));
|
|
356
|
+
assert.ok(pages.includes('terms.html'), 'terms.html must be among the scanned pages');
|
|
357
|
+
|
|
358
|
+
// The superseded spellings task 1003936 swept: Discipline, Craft, Type and lane.
|
|
359
|
+
// Three of the four can be matched as bare words — none has an innocent use on these
|
|
360
|
+
// pages (verified: zero hits for lane, and the craft/discipline hits were this bug).
|
|
361
|
+
const BARE = [
|
|
362
|
+
[/\bdisciplines?\b/i, 'discipline'],
|
|
363
|
+
[/\bcrafts?\b/i, 'craft'],
|
|
364
|
+
[/\blanes?\b/i, 'lane'],
|
|
365
|
+
];
|
|
366
|
+
// "type" and "domain" CANNOT be matched bare, and this is the interesting case rather
|
|
367
|
+
// than an omission. Both have innocent uses here that must survive: projects.html says
|
|
368
|
+
// "point a domain you own at it" (DNS) and "type the name here" (the verb), index.html
|
|
369
|
+
// has a "Types of projects" heading and projects.html a "Type" column (a PROJECT's
|
|
370
|
+
// type, a different axis entirely). A bare scan would flag four correct strings, and
|
|
371
|
+
// the exception list needed to silence them is exactly where the next stale spelling
|
|
372
|
+
// would hide. So match the SHAPES the role axis takes in a sentence instead.
|
|
373
|
+
const AXIS_SHAPED = [
|
|
374
|
+
[/\btypes?\s+you\s+worked\s+in\b/i, 'type ("the types you worked in")'],
|
|
375
|
+
[/\bdomains?\s+you\s+worked\s+in\b/i, 'domain ("the domains you worked in")'],
|
|
376
|
+
[/\bby\s+(type|lane|craft|discipline|domain)\b/i, 'the axis in a "by <word>" filter label'],
|
|
377
|
+
[/\byour\s+(type|lane|craft|discipline)\b/i, 'the axis in a "your <word>" label'],
|
|
378
|
+
];
|
|
379
|
+
for (const page of pages) {
|
|
380
|
+
const text = renderedText(page);
|
|
381
|
+
for (const [re, word] of BARE) {
|
|
382
|
+
assert.ok(!re.test(text), `${page} still spells the role axis "${word}"`);
|
|
383
|
+
}
|
|
384
|
+
for (const [re, what] of AXIS_SHAPED) {
|
|
385
|
+
assert.ok(!re.test(text), `${page} still spells the role axis as ${what}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// The negative assertions above all pass on a page that simply deleted the sentence,
|
|
390
|
+
// so pin the POSITIVE too — the clause must still be there, saying "roles".
|
|
391
|
+
assert.match(
|
|
392
|
+
renderedText('terms.html'),
|
|
393
|
+
/the rank that project gave you, the roles you worked in, and when you were last active/,
|
|
394
|
+
'terms.html no longer states, in the shipped wording, which role axis the platform publishes',
|
|
395
|
+
);
|
|
396
|
+
});
|
|
@@ -102,6 +102,13 @@ function harness({ count }) {
|
|
|
102
102
|
matchMedia: () => ({ matches: false, addEventListener() {}, addListener() {} }),
|
|
103
103
|
OTBBranding: { currencyLabel: () => 'Credits' },
|
|
104
104
|
OTB: {
|
|
105
|
+
// task 1003936 — the role axis vocabulary (dom-utils OTB.role*). PRESENT for
|
|
106
|
+
// the same reason categoryChip is: an undefined helper throws mid-render.
|
|
107
|
+
roleAxisLabel: () => 'Role',
|
|
108
|
+
roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
|
|
109
|
+
roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
|
|
110
|
+
roleIsUnset: (d) => !d || d === 'unclassified',
|
|
111
|
+
ROLE_UNSET: 'unclassified',
|
|
105
112
|
escapeHtml: (s) => String(s == null ? '' : s),
|
|
106
113
|
$: (sel) => documentObj.querySelector(sel),
|
|
107
114
|
num: (n) => Number(n) || 0,
|
|
@@ -517,6 +517,13 @@ function bootProfile({ signedIn = true } = {}) {
|
|
|
517
517
|
BongosClient: { createClient: () => client },
|
|
518
518
|
OTBKit: kit,
|
|
519
519
|
OTB: {
|
|
520
|
+
// task 1003936 — the role axis vocabulary (dom-utils OTB.role*). PRESENT for
|
|
521
|
+
// the same reason categoryChip is: an undefined helper throws mid-render.
|
|
522
|
+
roleAxisLabel: () => 'Role',
|
|
523
|
+
roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
|
|
524
|
+
roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
|
|
525
|
+
roleIsUnset: (d) => !d || d === 'unclassified',
|
|
526
|
+
ROLE_UNSET: 'unclassified',
|
|
520
527
|
escapeHtml: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])),
|
|
521
528
|
$: (sel) => documentObj.querySelector(sel),
|
|
522
529
|
fmtNum: (n) => String(n ?? 0),
|
|
@@ -79,6 +79,13 @@ async function boot({ search = '', me, profile } = {}) {
|
|
|
79
79
|
__BRANDING__: {},
|
|
80
80
|
BongosClient: { createClient: () => api },
|
|
81
81
|
OTB: {
|
|
82
|
+
// task 1003936 — the role axis vocabulary (dom-utils OTB.role*). PRESENT for
|
|
83
|
+
// the same reason categoryChip is: an undefined helper throws mid-render.
|
|
84
|
+
roleAxisLabel: () => 'Role',
|
|
85
|
+
roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
|
|
86
|
+
roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
|
|
87
|
+
roleIsUnset: (d) => !d || d === 'unclassified',
|
|
88
|
+
ROLE_UNSET: 'unclassified',
|
|
82
89
|
$, escapeHtml,
|
|
83
90
|
fmtNum: (n) => String(Number(n) || 0),
|
|
84
91
|
fmtDate: (d) => String(d).slice(0, 10),
|
|
@@ -161,7 +168,8 @@ test('full cross_project → tiles, named rows, plain-text standing, +N more', a
|
|
|
161
168
|
assert.match(html, /Metic/, 'rank_held renders capitalized as that project\'s own standing');
|
|
162
169
|
assert.ok(!html.includes('rank-badge'), 'never the local rank-badge component for another project\'s rank (spec Decision 1)');
|
|
163
170
|
assert.match(html, /\+ 1 more project/, 'non-public projects are counted, never named');
|
|
164
|
-
|
|
171
|
+
// task 1003936: one word for the axis, and the values are named not slugged.
|
|
172
|
+
assert.match(html, /Roles: Engineer, UI/);
|
|
165
173
|
assert.match(html, /Last active 2026-08-01/);
|
|
166
174
|
assert.ok(!html.includes('/settings#cross-project'), 'no self link for a stranger');
|
|
167
175
|
});
|
|
@@ -28,6 +28,13 @@ const require = createRequire(import.meta.url);
|
|
|
28
28
|
const P = require('../scripts/gds/provision.js');
|
|
29
29
|
const D = require('../scripts/gds/provision-disconnect.js');
|
|
30
30
|
|
|
31
|
+
// The assertions below pin the supervision verbs as `systemctl …` (task 1003997, ADR 0306).
|
|
32
|
+
// The manager is PROBED from /run/systemd/system when nothing pins it, so a CI runner that
|
|
33
|
+
// boots systemd and a container that does not disagree on the same source. Pin it here — these
|
|
34
|
+
// tests are about the runbook, not about what the test box happens to boot. The probe itself is
|
|
35
|
+
// covered directly, with an injected fs, in tests/provision.mjs.
|
|
36
|
+
process.env.PROVISION_PROCESS_MANAGER = 'systemd';
|
|
37
|
+
|
|
31
38
|
let passed = 0, failed = 0;
|
|
32
39
|
function t(name, fn) {
|
|
33
40
|
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
@@ -32,6 +32,13 @@ const require = createRequire(import.meta.url);
|
|
|
32
32
|
const P = require('../scripts/gds/provision.js');
|
|
33
33
|
const { MANIFEST_VERIFY_TRIES } = require('../scripts/gds/provision-config.js');
|
|
34
34
|
|
|
35
|
+
// The assertions below pin the supervision verbs as `systemctl …` (task 1003997, ADR 0306).
|
|
36
|
+
// The manager is PROBED from /run/systemd/system when nothing pins it, so a CI runner that
|
|
37
|
+
// boots systemd and a container that does not disagree on the same source. Pin it here — these
|
|
38
|
+
// tests are about the runbook, not about what the test box happens to boot. The probe itself is
|
|
39
|
+
// covered directly, with an injected fs, in tests/provision.mjs.
|
|
40
|
+
process.env.PROVISION_PROCESS_MANAGER = 'systemd';
|
|
41
|
+
|
|
35
42
|
let passed = 0, failed = 0;
|
|
36
43
|
async function ta(name, fn) {
|
|
37
44
|
try { await fn(); passed++; console.log(` PASS ${name}`); }
|
|
@@ -28,6 +28,13 @@ const require = createRequire(import.meta.url);
|
|
|
28
28
|
const P = require('../scripts/gds/provision.js');
|
|
29
29
|
const { MANIFEST_VERIFY_TRIES } = require('../scripts/gds/provision-config.js');
|
|
30
30
|
|
|
31
|
+
// The assertions below pin the supervision verbs as `systemctl …` (task 1003997, ADR 0306).
|
|
32
|
+
// The manager is PROBED from /run/systemd/system when nothing pins it, so a CI runner that
|
|
33
|
+
// boots systemd and a container that does not disagree on the same source. Pin it here — these
|
|
34
|
+
// tests are about the runbook, not about what the test box happens to boot. The probe itself is
|
|
35
|
+
// covered directly, with an injected fs, in tests/provision.mjs.
|
|
36
|
+
process.env.PROVISION_PROCESS_MANAGER = 'systemd';
|
|
37
|
+
|
|
31
38
|
const NEUTRAL_PREFIX =
|
|
32
39
|
JSON.parse(readFileSync(new URL('../config/branding.neutral.json', import.meta.url), 'utf8')).envPrefix || 'CLOUDBONGOS';
|
|
33
40
|
|
|
@@ -118,6 +118,13 @@ function harness({ total = 320, failAfter = null, viewerRank = 'metic', perPageP
|
|
|
118
118
|
scrollTo() {},
|
|
119
119
|
addEventListener() {},
|
|
120
120
|
OTB: {
|
|
121
|
+
// task 1003936 — the role axis vocabulary (dom-utils OTB.role*). PRESENT for
|
|
122
|
+
// the same reason categoryChip is: an undefined helper throws mid-render.
|
|
123
|
+
roleAxisLabel: () => 'Role',
|
|
124
|
+
roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
|
|
125
|
+
roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
|
|
126
|
+
roleIsUnset: (d) => !d || d === 'unclassified',
|
|
127
|
+
ROLE_UNSET: 'unclassified',
|
|
121
128
|
escapeHtml: (s) => String(s == null ? '' : s),
|
|
122
129
|
$: (sel) => documentObj.querySelector(sel),
|
|
123
130
|
num: (n) => Number(n) || 0,
|
|
@@ -159,6 +159,13 @@ async function boot(opts = {}) {
|
|
|
159
159
|
const windowObj = {
|
|
160
160
|
__BRANDING__: { project: { joinability: 'apply' } },
|
|
161
161
|
OTB: {
|
|
162
|
+
// task 1003936 — the role axis vocabulary (dom-utils OTB.role*). PRESENT for
|
|
163
|
+
// the same reason categoryChip is: an undefined helper throws mid-render.
|
|
164
|
+
roleAxisLabel: () => 'Role',
|
|
165
|
+
roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
|
|
166
|
+
roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
|
|
167
|
+
roleIsUnset: (d) => !d || d === 'unclassified',
|
|
168
|
+
ROLE_UNSET: 'unclassified',
|
|
162
169
|
escapeHtml,
|
|
163
170
|
$: (sel) => documentObj.querySelector(sel),
|
|
164
171
|
num: (v) => Number(v),
|
|
@@ -129,6 +129,13 @@ async function boot(opts = {}) {
|
|
|
129
129
|
const windowObj = {
|
|
130
130
|
__BRANDING__: { project: { joinability: 'apply' } },
|
|
131
131
|
OTB: {
|
|
132
|
+
// task 1003936 — the role axis vocabulary (dom-utils OTB.role*). PRESENT for
|
|
133
|
+
// the same reason categoryChip is: an undefined helper throws mid-render.
|
|
134
|
+
roleAxisLabel: () => 'Role',
|
|
135
|
+
roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
|
|
136
|
+
roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
|
|
137
|
+
roleIsUnset: (d) => !d || d === 'unclassified',
|
|
138
|
+
ROLE_UNSET: 'unclassified',
|
|
132
139
|
escapeHtml,
|
|
133
140
|
$: (sel) => documentObj.querySelector(sel),
|
|
134
141
|
num: (v) => Number(v),
|
|
@@ -116,6 +116,13 @@ async function boot({ answers = {}, fallback = ok({}) } = {}) {
|
|
|
116
116
|
const windowObj = {
|
|
117
117
|
__BRANDING__: { project: { joinability: 'apply' } },
|
|
118
118
|
OTB: {
|
|
119
|
+
// task 1003936 — the role axis vocabulary (dom-utils OTB.role*). PRESENT for
|
|
120
|
+
// the same reason categoryChip is: an undefined helper throws mid-render.
|
|
121
|
+
roleAxisLabel: () => 'Role',
|
|
122
|
+
roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
|
|
123
|
+
roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
|
|
124
|
+
roleIsUnset: (d) => !d || d === 'unclassified',
|
|
125
|
+
ROLE_UNSET: 'unclassified',
|
|
119
126
|
escapeHtml,
|
|
120
127
|
$: (sel) => documentObj.querySelector(sel),
|
|
121
128
|
num: (v) => Number(v),
|