@bongos/core 1.19.345 → 1.19.347
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 +55 -15
- package/docs/copy-inventory.md +27 -15
- package/docs/copy-registry.json +145 -17
- package/modules/hall-ui/public/collab-lib.js +77 -0
- package/modules/hall-ui/public/collab.css +36 -0
- package/modules/hall-ui/public/collab.html +66 -0
- package/modules/hall-ui/public/collab.js +139 -0
- package/modules/hall-ui/public/shell.js +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/goal-names.js +102 -0
- package/scripts/gds/rename-goals.js +109 -0
- package/src/bongos/serve-internal.js +4 -1
- package/src/module-api.js +5 -1
- package/tests/collab_page.mjs +172 -0
- package/tests/goal_names.mjs +120 -0
- package/tests/hall_nav.mjs +7 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.347",
|
|
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",
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/goal-names.js — give every open goal a NAME, and move its
|
|
3
|
+
// explanation into `subtitle` (task 1003225, goal 1000072, criterion
|
|
4
|
+
// legibility-goal-names-and-claude-text).
|
|
5
|
+
//
|
|
6
|
+
// Owner directive: a goal's subtitle should not sit on its title. 28 open goals
|
|
7
|
+
// carried theirs after a dash and 3 more after a colon — the punctuation is not
|
|
8
|
+
// the point, the jamming is. Task 1003224 gave `goals` a `subtitle` column and
|
|
9
|
+
// taught the hall to render name / subtitle / Claude's prose at three weights;
|
|
10
|
+
// this moves the existing text into that shape.
|
|
11
|
+
//
|
|
12
|
+
// WHY A MAPPING FILE AND NOT 31 AD-HOC PATCHES. A rename is a judgment call, and
|
|
13
|
+
// judgment that lives only in a shell history cannot be reviewed, re-run, or
|
|
14
|
+
// argued with. The table below IS the decision: it can be read in a diff, its
|
|
15
|
+
// invariants are unit-tested (no duplicate names, no name still carrying its own
|
|
16
|
+
// explanation, nothing dropped), and re-running is a no-op.
|
|
17
|
+
//
|
|
18
|
+
// IT IS NOT A MECHANICAL SPLIT, which is the whole reason this is its own task.
|
|
19
|
+
// Splitting on the separator works where the head already names the thing
|
|
20
|
+
// ("Findability — the grep path"). It FAILS where the head is not distinctive:
|
|
21
|
+
// goals 1000038/1000039/1000040 all head "Agents module" and differ ONLY in the
|
|
22
|
+
// tail, so a split would leave three identically-named goals — strictly worse
|
|
23
|
+
// than today. Those three, the version catch-all, and three wordy heads get
|
|
24
|
+
// written names; the rest keep theirs verbatim.
|
|
25
|
+
//
|
|
26
|
+
// SAFETY. Dry-run by DEFAULT (`--apply` to write). Every write goes through
|
|
27
|
+
// PATCH /goals/:id, which is authorized (owner|manager|Archon) and records
|
|
28
|
+
// prose-edit deltas — so the rename is audit-trailed, unlike the prod SQL UPDATE
|
|
29
|
+
// that would otherwise be the only channel. It REFUSES to run at all if the
|
|
30
|
+
// server does not yet return `subtitle`, because renaming titles against a
|
|
31
|
+
// server that cannot store the subtitle would DELETE the explanations.
|
|
32
|
+
|
|
33
|
+
'use strict';
|
|
34
|
+
|
|
35
|
+
// id → { title, subtitle, why? }. `why` is recorded only where the name is
|
|
36
|
+
// NEW rather than the existing head — i.e. where judgment was applied.
|
|
37
|
+
const GOAL_NAMES = Object.freeze({
|
|
38
|
+
// ── names written, because the head was not usable ───────────────────────
|
|
39
|
+
1000003: { title: 'Work with no goal yet',
|
|
40
|
+
subtitle: 'The version catch-all. Tasks land here when nothing else fits — and nothing here can satisfy a criterion (ADR 0183), so it is a holding pen, not a home.',
|
|
41
|
+
why: 'was "BONGOS-V1 — general": a version id and the word general name nothing. Named for what it actually is.' },
|
|
42
|
+
1000038: { title: 'Agent framework & registry',
|
|
43
|
+
subtitle: 'The foundation — the plugin surface an agent is written against, and the registry that holds the roster.',
|
|
44
|
+
why: 'one of THREE goals heading "Agents module"; a mechanical split would have left three identically-named goals. Written without a colon on purpose: "Agents: framework" is the same name-plus-qualifier shape this task exists to remove, and the test flagged it.' },
|
|
45
|
+
1000039: { title: 'Agent authoring & first roster',
|
|
46
|
+
subtitle: 'Owner and builder authoring, plus the first real roster — event-driven and on-demand agents.',
|
|
47
|
+
why: 'second of the three "Agents module" goals. Named for what it adds — authoring and the first roster — rather than by a tail no list ever showed.' },
|
|
48
|
+
1000040: { title: 'Agent scheduling & spend limits',
|
|
49
|
+
subtitle: 'Scheduled agents, and the guardrails on what they are allowed to spend.',
|
|
50
|
+
why: 'third of the three "Agents module" goals. Scheduling and spend are what separate it from the framework and authoring goals.' },
|
|
51
|
+
1000053: { title: 'Sessions start knowing',
|
|
52
|
+
subtitle: 'Front-load auth, environment and instance facts so a session does not spend its first minutes rediscovering them.',
|
|
53
|
+
why: 'was "Session efficiency: sessions start knowing" — the head restated the tail. Kept the half that is a name.' },
|
|
54
|
+
1000064: { title: 'The hall as the room',
|
|
55
|
+
subtitle: 'Agora, Monitor & Contribute, and chat that does not care which channel it came from.',
|
|
56
|
+
why: 'colon-separated rather than dashed; same jamming, so treated identically.' },
|
|
57
|
+
1000075: { title: 'Collaboration & the social hall',
|
|
58
|
+
subtitle: 'Ask for help by craft, and let the hall become a mode rather than a page.',
|
|
59
|
+
why: 'was "Collab, then the social hall" — an ordering, not a name.' },
|
|
60
|
+
1000077: { title: 'Guards, not notes',
|
|
61
|
+
subtitle: 'The four failure modes that prose already documented and nothing refused.',
|
|
62
|
+
why: 'colon-separated; the head is already a good name, the tail is the explanation.' },
|
|
63
|
+
1000078: { title: 'Project money out',
|
|
64
|
+
subtitle: 'A funded balance, real builder payouts, visible outflows, and work that cannot outrun the money.',
|
|
65
|
+
why: 'colon-separated rather than dashed. The head is already the name; the tail is a four-part explanation that belongs underneath it.' },
|
|
66
|
+
|
|
67
|
+
// ── heads kept verbatim; only the tail moves ─────────────────────────────
|
|
68
|
+
1000025: { title: 'Builder experience', subtitle: 'Hall, dev box, and skills.' },
|
|
69
|
+
1000027: { title: 'Findability', subtitle: 'The grep path (ADR 0095).' },
|
|
70
|
+
1000029: { title: 'Module upstreaming & catalog', subtitle: 'Instances contribute modules back into core.' },
|
|
71
|
+
1000032: { title: 'Instance hosting + domains', subtitle: 'The `provisioning` module.' },
|
|
72
|
+
1000044: { title: 'Living project sky', subtitle: 'A shared, persistent, data-driven orb gate.' },
|
|
73
|
+
1000045: { title: 'Public builder profiles', subtitle: 'Track record, apply-to-project, and owner recruiting.' },
|
|
74
|
+
1000046: { title: 'Project creation', subtitle: 'Visibility and customization.' },
|
|
75
|
+
1000048: { title: 'SEO & public discoverability', subtitle: 'Rankable per-project profiles.' },
|
|
76
|
+
1000049: { title: 'Orb Physics', subtitle: 'Orb motion and space-whale flight.' },
|
|
77
|
+
1000052: { title: 'First-run onboarding', subtitle: 'From a bare machine to a first ship.' },
|
|
78
|
+
1000054: { title: 'Public distribution', subtitle: 'CLI installer and Claude plugin.' },
|
|
79
|
+
1000057: { title: 'Rank blocker matrix', subtitle: 'Empirically verify intended vs unintended blockers across xenos, thetes and metic.' },
|
|
80
|
+
1000058: { title: 'Non-cloudbongos project lifecycle', subtitle: 'Creation, governance, planning, building — the whole flow works and feels right.' },
|
|
81
|
+
1000063: { title: 'The front door', subtitle: 'Enter the Future: hero-only landing, wormhole transit, About on the map.' },
|
|
82
|
+
1000065: { title: 'Community surface', subtitle: 'Builder discovery on the hub.' },
|
|
83
|
+
1000067: { title: 'Ideator compensation', subtitle: 'The idea credit stream, separate from cost-plus.' },
|
|
84
|
+
1000068: { title: 'Government, not governance', subtitle: 'Ranks are ranks, and "role" belongs to the crafts.' },
|
|
85
|
+
1000070: { title: 'Scan-before-install', subtitle: 'First-real-run hardening.' },
|
|
86
|
+
1000071: { title: 'Idea routing', subtitle: 'Work that has a home lands in it.' },
|
|
87
|
+
1000072: { title: 'Human legibility', subtitle: 'Seeing, sorting and steering an AI-run project.' },
|
|
88
|
+
1000073: { title: 'Finish the GDS→Bongos rename', subtitle: 'Retire a legacy Primea term without rewriting history.' },
|
|
89
|
+
1000074: { title: "The artist's loop", subtitle: 'Review authority, copy an artist can change, and a place to do it.' },
|
|
90
|
+
// 1000080 already carried a subtitle, so the tail is PREPENDED to it rather than
|
|
91
|
+
// replacing it — overwriting would delete text, the exact loss this task guards.
|
|
92
|
+
1000080: { title: 'Apex UI pass 2', subtitle: 'One top bar, two dropdowns, and a project card worth opening — the chrome and the card on cloudbongos.com and /projects.' },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// A name must not carry its own explanation — that is the whole point.
|
|
96
|
+
const SEPARATOR = / [—-] |: /;
|
|
97
|
+
|
|
98
|
+
// Goals deliberately untouched: their titles are already names. Listed so the
|
|
99
|
+
// coverage test can tell "already fine" from "forgotten".
|
|
100
|
+
const ALREADY_NAMES = Object.freeze([1000009, 1000024, 1000028, 1000037, 1000047, 1000050]);
|
|
101
|
+
|
|
102
|
+
module.exports = { GOAL_NAMES, ALREADY_NAMES, SEPARATOR };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/rename-goals.js — apply the goal-names.js mapping (task 1003225).
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// node scripts/gds/rename-goals.js # DRY RUN — prints the diff, writes nothing
|
|
6
|
+
// node scripts/gds/rename-goals.js --apply # writes, via PATCH /goals/:id
|
|
7
|
+
//
|
|
8
|
+
// Writes go through PATCH /goals/:id, never SQL: that route is authorized
|
|
9
|
+
// (owner|manager|Archon) and records prose-edit deltas, so a rename of somebody's
|
|
10
|
+
// goal is audit-trailed. A prod UPDATE would be invisible.
|
|
11
|
+
//
|
|
12
|
+
// THE GUARD THAT MATTERS: it refuses to run when the server does not return
|
|
13
|
+
// `subtitle`. Renaming a title against a server that cannot store the subtitle
|
|
14
|
+
// would DELETE the explanation — the exact data loss this task exists to avoid.
|
|
15
|
+
// core_224 ships the column; a live instance one release behind does not have it.
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const { cliClient, requireSession, hasFlag } = require('./cli-lib');
|
|
20
|
+
const { GOAL_NAMES, SEPARATOR } = require('./goal-names.js');
|
|
21
|
+
|
|
22
|
+
// Nothing to do when the goal already reads the way the mapping wants.
|
|
23
|
+
function planFor(goal, want) {
|
|
24
|
+
const patch = {};
|
|
25
|
+
if ((goal.title || '') !== want.title) patch.title = want.title;
|
|
26
|
+
if ((goal.subtitle || '') !== want.subtitle) patch.subtitle = want.subtitle;
|
|
27
|
+
return patch;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function main() {
|
|
31
|
+
await requireSession();
|
|
32
|
+
const apply = hasFlag('--apply');
|
|
33
|
+
const api = await cliClient();
|
|
34
|
+
|
|
35
|
+
const res = await api.goals.getGoals({ query: { version: 'BONGOS-V1' } });
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
console.error(`could not list goals: ${res.status}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const goals = (res.data.goals || []).filter((g) => g.status === 'open');
|
|
41
|
+
if (!goals.length) {
|
|
42
|
+
console.error('no open goals returned — refusing to act on an empty read.');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The refusal above all others.
|
|
47
|
+
if (!Object.prototype.hasOwnProperty.call(goals[0], 'subtitle')) {
|
|
48
|
+
console.error('REFUSING: this server does not return `subtitle`, so it cannot store one.');
|
|
49
|
+
console.error('Renaming titles now would drop every explanation on the floor.');
|
|
50
|
+
console.error('core_224 adds the column — wait for the release carrying it to DEPLOY, then re-run.');
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let changed = 0; let already = 0; let missing = 0; let failed = 0;
|
|
55
|
+
for (const g of goals.sort((a, b) => Number(a.id) - Number(b.id))) {
|
|
56
|
+
const want = GOAL_NAMES[Number(g.id)];
|
|
57
|
+
if (!want) {
|
|
58
|
+
// Not an error: goals whose titles were already names are deliberately absent.
|
|
59
|
+
if (SEPARATOR.test(g.title || '')) {
|
|
60
|
+
console.error(`! ${g.id} has a jammed title but no mapping entry: ${g.title}`);
|
|
61
|
+
missing += 1;
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const patch = planFor(g, want);
|
|
66
|
+
if (!Object.keys(patch).length) { already += 1; continue; }
|
|
67
|
+
console.log(`${apply ? '→' : '·'} ${g.id}`);
|
|
68
|
+
if (patch.title) console.log(` title ${JSON.stringify(g.title)} -> ${JSON.stringify(patch.title)}`);
|
|
69
|
+
if (patch.subtitle !== undefined) console.log(` subtitle ${JSON.stringify(g.subtitle || '')} -> ${JSON.stringify(patch.subtitle)}`);
|
|
70
|
+
if (!apply) { changed += 1; continue; }
|
|
71
|
+
const r = await api.goals.patchGoalsId({ id: g.id, body: patch });
|
|
72
|
+
if (!r.ok) {
|
|
73
|
+
console.error(` FAILED ${r.status} ${JSON.stringify(r.data)}`);
|
|
74
|
+
failed += 1;
|
|
75
|
+
} else {
|
|
76
|
+
changed += 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log('');
|
|
81
|
+
console.log(`${apply ? 'applied' : 'would change'}: ${changed} · already correct: ${already}`
|
|
82
|
+
+ `${missing ? ` · UNMAPPED jammed titles: ${missing}` : ''}${failed ? ` · FAILED: ${failed}` : ''}`);
|
|
83
|
+
if (!apply) console.log('dry run — re-run with --apply to write.');
|
|
84
|
+
|
|
85
|
+
// THE POST-CONDITION, CHECKED AGAINST REAL DATA — not against the mapping.
|
|
86
|
+
// The task's deliverable is a STATE of the live goal set ("no open goal's title
|
|
87
|
+
// contains a dash-appended subtitle"), and unit tests over the mapping's internal
|
|
88
|
+
// consistency cannot witness that: they would pass just as happily if --apply had
|
|
89
|
+
// never been run. This re-reads what the server actually holds now and reports it,
|
|
90
|
+
// so the claim is falsifiable by anyone re-running the command.
|
|
91
|
+
//
|
|
92
|
+
// SCOPED TO THIS VERSION, deliberately. The read above is `version: BONGOS-V1`;
|
|
93
|
+
// CB-V1 carries jammed titles of its own (id 4, 1000043, 1000066, 1000079) which
|
|
94
|
+
// are ANOTHER version's work, and sweeping them in here would rename goals this
|
|
95
|
+
// task never reviewed.
|
|
96
|
+
const stillJammed = goals.filter((g) => SEPARATOR.test(g.title || ''));
|
|
97
|
+
console.log('');
|
|
98
|
+
console.log(`post-condition (BONGOS-V1 open goals): ${goals.length} checked · `
|
|
99
|
+
+ `${stillJammed.length} still carrying an explanation in the title`);
|
|
100
|
+
for (const g of stillJammed) console.log(` ! ${g.id} ${JSON.stringify(g.title)}`);
|
|
101
|
+
|
|
102
|
+
process.exit(failed || missing ? 1 : 0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (require.main === module) {
|
|
106
|
+
main().catch((err) => { console.error('fatal:', err); process.exit(1); });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { planFor };
|
|
@@ -944,8 +944,11 @@ function mountInternalSurfaces(app) {
|
|
|
944
944
|
// URL→gate map by parsing this file statically, and a comment in that gap drops
|
|
945
945
|
// the regex out of its map — which reads as "every page on this tier is
|
|
946
946
|
// ungated". It fails loudly by design; keep prose above the const.
|
|
947
|
+
// task 1003172 (C02): `collab` joins the same tier — every read behind it is
|
|
948
|
+
// any-builder and scoped to the caller by the SERVER (GET /help-requests/for-me
|
|
949
|
+
// derives crafts from the viewer's own profile), so signed-in is the right floor.
|
|
947
950
|
const SIGNEDIN_BUILDERS_PAGE_RE =
|
|
948
|
-
/^\/builders\/(?:work|pair|settings|atlas|modules|goals|roadmap|copy-desk|ideas|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
|
|
951
|
+
/^\/builders\/(?:work|pair|settings|atlas|modules|goals|roadmap|copy-desk|ideas|collab|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
|
|
949
952
|
// The diagram image assets under /builders/diagrams/ ride the same tier as the
|
|
950
953
|
// page that embeds them.
|
|
951
954
|
const SIGNEDIN_BUILDERS_PREFIX_RE = /^\/builders\/diagrams\//;
|
package/src/module-api.js
CHANGED
|
@@ -1120,8 +1120,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1120
1120
|
// landed since 1.19.343 with no explicit bump. run 32799492873. (task 1002620)
|
|
1121
1121
|
// 1.19.345 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1122
1122
|
// landed since 1.19.344 with no explicit bump. run 32800819617. (task 1002620)
|
|
1123
|
+
// 1.19.346 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1124
|
+
// landed since 1.19.345 with no explicit bump. run 32801147433. (task 1002620)
|
|
1125
|
+
// 1.19.347 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1126
|
+
// landed since 1.19.346 with no explicit bump. run 32802265337. (task 1002620)
|
|
1123
1127
|
// ---------------------------------------------------------------------------
|
|
1124
|
-
const CORE_VERSION = '1.19.
|
|
1128
|
+
const CORE_VERSION = '1.19.347'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1125
1129
|
|
|
1126
1130
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1127
1131
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// tests/collab_page.mjs — the Collab page (task 1003172 / C02, goal 1000075,
|
|
2
|
+
// criterion collab-page-matches-my-craft).
|
|
3
|
+
//
|
|
4
|
+
// The criterion has three clauses and the third is the one that gets skipped:
|
|
5
|
+
// requests match the viewer's CRAFT, are ANSWERABLE from the page, and a request
|
|
6
|
+
// nobody takes is VISIBLY AGING rather than silently dead. Sorting the oldest to
|
|
7
|
+
// the top does not satisfy the third — the oldest then looks exactly like the
|
|
8
|
+
// newest, only lower down.
|
|
9
|
+
import { strict as assert } from 'node:assert';
|
|
10
|
+
import { test } from 'node:test';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
+
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
17
|
+
const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8');
|
|
18
|
+
|
|
19
|
+
// collab-lib.js is pure, so RUN it rather than grep it.
|
|
20
|
+
function lib() {
|
|
21
|
+
const src = read('modules', 'hall-ui', 'public', 'collab-lib.js');
|
|
22
|
+
const win = {};
|
|
23
|
+
new Function('window', src)(win);
|
|
24
|
+
return win.OTBCollabLib;
|
|
25
|
+
}
|
|
26
|
+
const NOW = Date.parse('2026-08-24T12:00:00Z');
|
|
27
|
+
const daysAgo = (d) => new Date(NOW - d * 86400000).toISOString();
|
|
28
|
+
|
|
29
|
+
// ---- THE AGEING CLAUSE ----------------------------------------------------
|
|
30
|
+
test('age is a BAND with a label, not just a sort position', () => {
|
|
31
|
+
const L = lib();
|
|
32
|
+
assert.equal(L.ageBand(daysAgo(0), NOW).id, 'fresh');
|
|
33
|
+
assert.equal(L.ageBand(daysAgo(1), NOW).id, 'waiting');
|
|
34
|
+
assert.equal(L.ageBand(daysAgo(3), NOW).id, 'aging');
|
|
35
|
+
assert.equal(L.ageBand(daysAgo(7), NOW).id, 'stale');
|
|
36
|
+
assert.equal(L.ageBand(daysAgo(90), NOW).id, 'stale');
|
|
37
|
+
for (const b of L.BANDS) assert.ok(b.label && b.label.length > 4, `${b.id} says it in words`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('the boundaries are exact, so the bands mean something', () => {
|
|
41
|
+
const L = lib();
|
|
42
|
+
assert.equal(L.ageBand(daysAgo(0.99), NOW).id, 'fresh', 'just under a day is still fresh');
|
|
43
|
+
assert.equal(L.ageBand(daysAgo(2.99), NOW).id, 'waiting');
|
|
44
|
+
assert.equal(L.ageBand(daysAgo(6.99), NOW).id, 'aging');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('an unknown age reads as fresh, never as an accusation', () => {
|
|
48
|
+
const L = lib();
|
|
49
|
+
for (const bad of [null, undefined, '', 'nonsense']) {
|
|
50
|
+
assert.equal(L.ageBand(bad, NOW).id, 'fresh', `${JSON.stringify(bad)} must not look neglected`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('ageing changes how a row LOOKS, not only where it sits', () => {
|
|
55
|
+
// The whole point of the clause. Each band must have its own visual treatment
|
|
56
|
+
// in CSS, or "visibly aging" is just a sort order with extra steps.
|
|
57
|
+
const css = read('modules', 'hall-ui', 'public', 'collab.css');
|
|
58
|
+
const L = lib();
|
|
59
|
+
for (const b of L.BANDS) {
|
|
60
|
+
assert.match(css, new RegExp(`\\.cb-item--${b.id}\\b`), `${b.id} has its own styling`);
|
|
61
|
+
}
|
|
62
|
+
assert.match(css, /\.cb-item--stale\s*\{[^}]*border-left-color/, 'the band reads down a list at a glance');
|
|
63
|
+
// Colour alone is not a message and not everyone sees it.
|
|
64
|
+
assert.match(css, /\.cb-item--stale \.cb-age/, 'and the words are emphasised too');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('the row markup carries the band class and the label', () => {
|
|
68
|
+
const js = read('modules', 'hall-ui', 'public', 'collab.js');
|
|
69
|
+
assert.match(js, /cb-item--\$\{L\.ageBand\(/, 'the class comes from the band');
|
|
70
|
+
assert.match(js, /cb-age cb-age--\$\{band\.id\}/, 'and so does the chip');
|
|
71
|
+
assert.match(js, /escapeHtml\(band\.label\)/, 'the label is rendered, escaped');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('oldest first WITHIN the page, in addition to the band', () => {
|
|
75
|
+
const L = lib();
|
|
76
|
+
const rows = [{ created_at: daysAgo(1) }, { created_at: daysAgo(9) }, { created_at: daysAgo(4) }];
|
|
77
|
+
const sorted = rows.slice().sort(L.byAgeOldestFirst);
|
|
78
|
+
assert.deepEqual(sorted.map((r) => L.ageBand(r.created_at, NOW).id), ['stale', 'aging', 'waiting']);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ---- THE MATCHING CLAUSE --------------------------------------------------
|
|
82
|
+
test('the page NEVER sends a craft — matching is the server\'s job', () => {
|
|
83
|
+
// C01's grader found a ?craft= param that let any builder read another craft's
|
|
84
|
+
// requests. The fix put craft derivation on the server, and this page must not
|
|
85
|
+
// reintroduce the parameter from the client side.
|
|
86
|
+
const js = read('modules', 'hall-ui', 'public', 'collab.js');
|
|
87
|
+
// Assert on the REQUEST, not on any occurrence of the word: the first version
|
|
88
|
+
// matched `Matching your craft: ${crafts.join(', ')}` — the page's own subtitle —
|
|
89
|
+
// which is display text, not a parameter. Fifth time this session that a source
|
|
90
|
+
// assertion has been wider than its invariant (learning 1000196).
|
|
91
|
+
assert.match(js, /getHelpRequestsForMe\(\{\}\)/, 'the read is unparameterised');
|
|
92
|
+
assert.ok(!/(query|body):\s*\{[^}]*craft/.test(js), 'craft never travels in a query or body');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('a craft request and a by-name request read differently', () => {
|
|
96
|
+
const L = lib();
|
|
97
|
+
assert.match(L.addressedTo({ needs_craft: 'artist' }, 1), /artists/);
|
|
98
|
+
assert.match(L.addressedTo({ needs_builder_id: 7 }, 7), /by name/);
|
|
99
|
+
assert.match(L.addressedTo({ needs_builder_id: 7 }, 9), /one person/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('context links to the task or goal, and to neither when absent', () => {
|
|
103
|
+
const L = lib();
|
|
104
|
+
assert.equal(L.contextOf({ task_id: 5 }).kind, 'task');
|
|
105
|
+
assert.equal(L.contextOf({ goal_id: 5 }).kind, 'goal');
|
|
106
|
+
assert.equal(L.contextOf({}), null, 'no context means no element, not an empty one');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ---- THE ANSWERABLE CLAUSE ------------------------------------------------
|
|
110
|
+
test('the asker can settle in place, with both outcomes', () => {
|
|
111
|
+
const js = read('modules', 'hall-ui', 'public', 'collab.js');
|
|
112
|
+
assert.match(js, /data-settle="answered"/);
|
|
113
|
+
assert.match(js, /data-settle="withdrawn"/);
|
|
114
|
+
assert.match(js, /patchHelpRequestsId/, 'through the real API, not a local edit');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('there is deliberately NO reply thread — that would reimplement the Agora', () => {
|
|
118
|
+
// The goal forbids reimplementing the Agora to get around C06's dependency.
|
|
119
|
+
const js = read('modules', 'hall-ui', 'public', 'collab.js');
|
|
120
|
+
for (const f of ['reply', 'comment', 'thread', 'message']) {
|
|
121
|
+
assert.ok(!new RegExp(`data-${f}|post${f}`, 'i').test(js), `no ${f} surface`);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ---- placement ------------------------------------------------------------
|
|
126
|
+
test('it is a PAGE, not a Home card — the actionable half is the needs roster', () => {
|
|
127
|
+
// hall-ui/CLAUDE.md's tree: an action awaiting the builder goes in the
|
|
128
|
+
// needs-attention inbox and NEVER a new card, because one landing place for
|
|
129
|
+
// everything actionable is the whole point. C01 already put it there.
|
|
130
|
+
assert.ok(fs.existsSync(path.join(HALL, 'collab.html')));
|
|
131
|
+
const builders = read('modules', 'hall-ui', 'public', 'builders.js');
|
|
132
|
+
assert.ok(!/collab/i.test(builders), 'no Home card was added');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('the page is behind the signed-in gate', () => {
|
|
136
|
+
const serve = read('src', 'bongos', 'routes', '..', 'serve-internal.js');
|
|
137
|
+
const m = /const SIGNEDIN_BUILDERS_PAGE_RE =\s*(\/[^\n]+\/);/.exec(serve);
|
|
138
|
+
assert.ok(m, 'the gate const is still statically parseable');
|
|
139
|
+
assert.ok(new RegExp(m[1].slice(1, -1)).test('/builders/collab'), 'collab is gated');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('every CSS token the page uses exists in style.css', () => {
|
|
143
|
+
const css = read('modules', 'hall-ui', 'public', 'collab.css');
|
|
144
|
+
const style = read('modules', 'hall-ui', 'public', 'style.css');
|
|
145
|
+
const used = new Set([...css.matchAll(/var\((--[a-z-]+)\)/g)].map((x) => x[1]));
|
|
146
|
+
assert.ok(used.size >= 5);
|
|
147
|
+
for (const t of used) {
|
|
148
|
+
assert.ok(new RegExp(`^\\s*${t}:`, 'm').test(style), `${t} must be defined (idea 1000821)`);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ---- THE CLIENT ACTUALLY REACHES THE API ----------------------------------
|
|
153
|
+
// The page called every endpoint through the GENERATED client (api.helpRequests.*),
|
|
154
|
+
// whose methods emit API-RELATIVE paths ('/help-requests/for-me'). Combined with
|
|
155
|
+
// baseUrl:'' — which the client documents as "these paths are already absolute" —
|
|
156
|
+
// every request went to the SITE ROOT and 404'd, so the page would have shipped
|
|
157
|
+
// dead on arrival. ideas.js and idea.js already shipped with exactly this defect.
|
|
158
|
+
//
|
|
159
|
+
// Grepped rather than executed on purpose: the defect is in URL CONSTRUCTION, and
|
|
160
|
+
// every existing test stubs the client, which is precisely why three pages shipped
|
|
161
|
+
// green while 404ing. A repo-wide version of this guard belongs in fitness.js once
|
|
162
|
+
// the two idea pages are fixed — scoped here to the page this task owns.
|
|
163
|
+
test('the client is not configured to send API calls to the site root', () => {
|
|
164
|
+
const js = read('modules', 'hall-ui', 'public', 'collab.js');
|
|
165
|
+
const namespaced = [...js.matchAll(/\bapi\.([a-zA-Z]\w*)\.([a-zA-Z]\w*)\(/g)];
|
|
166
|
+
assert.ok(namespaced.length > 0, 'this page uses the generated client');
|
|
167
|
+
assert.ok(
|
|
168
|
+
!/createClient\(\{[^}]*baseUrl:\s*(''|"")/.test(js),
|
|
169
|
+
"baseUrl:'' sends generated-client paths to the site root — drop it so they "
|
|
170
|
+
+ 'resolve against /api/bongos/v1, or switch every call to api.request() with a full path',
|
|
171
|
+
);
|
|
172
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// tests/goal_names.mjs — the goal rename mapping (task 1003225, goal 1000072,
|
|
2
|
+
// criterion legibility-goal-names-and-claude-text).
|
|
3
|
+
//
|
|
4
|
+
// The mapping IS the decision, so these tests are the review that a shell
|
|
5
|
+
// history could never get: no name may still carry its own explanation, no two
|
|
6
|
+
// goals may end up with the same name, and re-running must be a no-op.
|
|
7
|
+
import { strict as assert } from 'node:assert';
|
|
8
|
+
import { test } from 'node:test';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
15
|
+
const { GOAL_NAMES, ALREADY_NAMES, SEPARATOR } = require(path.join(ROOT, 'scripts', 'gds', 'goal-names.js'));
|
|
16
|
+
const { planFor } = require(path.join(ROOT, 'scripts', 'gds', 'rename-goals.js'));
|
|
17
|
+
|
|
18
|
+
const entries = () => Object.entries(GOAL_NAMES).map(([id, v]) => [Number(id), v]);
|
|
19
|
+
|
|
20
|
+
test('THE POINT: no new title carries its own explanation', () => {
|
|
21
|
+
// A dash or a colon in the name means the split did not happen.
|
|
22
|
+
for (const [id, v] of entries()) {
|
|
23
|
+
assert.ok(!SEPARATOR.test(v.title), `${id} still jams its explanation into the name: ${v.title}`);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('no two goals end up with the same name', () => {
|
|
28
|
+
// The failure a mechanical split would have produced: 1000038/39/40 all head
|
|
29
|
+
// "Agents module", so splitting on the separator leaves three identical names.
|
|
30
|
+
const seen = new Map();
|
|
31
|
+
for (const [id, v] of entries()) {
|
|
32
|
+
const key = v.title.toLowerCase();
|
|
33
|
+
assert.ok(!seen.has(key), `${id} and ${seen.get(key)} would both be named "${v.title}"`);
|
|
34
|
+
seen.set(key, id);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('the three Agents goals are genuinely distinguishable', () => {
|
|
39
|
+
const [a, b, c] = [1000038, 1000039, 1000040].map((id) => GOAL_NAMES[id]);
|
|
40
|
+
for (const g of [a, b, c]) assert.ok(g, 'all three are mapped');
|
|
41
|
+
const names = new Set([a.title, b.title, c.title]);
|
|
42
|
+
assert.equal(names.size, 3, 'three distinct names');
|
|
43
|
+
// And each says what IT is, not merely that it is an agents goal.
|
|
44
|
+
for (const g of [a, b, c]) {
|
|
45
|
+
assert.ok(g.title.length > 'Agents module'.length, `"${g.title}" must add something`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('every goal keeps an explanation — nothing is dropped', () => {
|
|
50
|
+
for (const [id, v] of entries()) {
|
|
51
|
+
assert.equal(typeof v.subtitle, 'string', `${id} has a subtitle`);
|
|
52
|
+
assert.ok(v.subtitle.trim().length >= 20, `${id}'s subtitle must carry the real explanation, not a stub`);
|
|
53
|
+
assert.notEqual(v.subtitle.trim().toLowerCase(), v.title.trim().toLowerCase(), `${id} must not restate its name`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('a name reads as a name, not a sentence', () => {
|
|
58
|
+
for (const [id, v] of entries()) {
|
|
59
|
+
assert.ok(v.title.length <= 40, `${id} "${v.title}" is ${v.title.length} chars — too long to scan in a list`);
|
|
60
|
+
assert.ok(!v.title.endsWith('.'), `${id} "${v.title}" should not be punctuated like prose`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('every name written with JUDGMENT records why', () => {
|
|
65
|
+
// Where the new name is not simply the old head, the reason is part of the
|
|
66
|
+
// artifact — that is what makes this reviewable rather than asserted.
|
|
67
|
+
for (const id of [1000003, 1000038, 1000039, 1000040, 1000053, 1000064, 1000075, 1000077, 1000078]) {
|
|
68
|
+
const v = GOAL_NAMES[id];
|
|
69
|
+
assert.ok(v, `${id} is mapped`);
|
|
70
|
+
assert.ok(v.why && v.why.length > 25, `${id} must record why its name was rewritten`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('the mapping and the leave-alone list are disjoint', () => {
|
|
75
|
+
for (const id of ALREADY_NAMES) {
|
|
76
|
+
assert.ok(!GOAL_NAMES[id], `${id} cannot be both renamed and deliberately untouched`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('mapping + leave-alone accounts for every open goal', () => {
|
|
81
|
+
// 38 = 32 mapping entries + 6 already-names. This is a fact about THIS FILE, not
|
|
82
|
+
// about how many goals are currently open — the two drifted apart and saying
|
|
83
|
+
// otherwise is how the first version of this comment became false.
|
|
84
|
+
//
|
|
85
|
+
// The live BONGOS-V1 open set is 37: goal 1000077 was mapped and renamed while
|
|
86
|
+
// open and has since been ACHIEVED, so 31 of the 32 entries have an open goal to
|
|
87
|
+
// apply to (hence the applier's 'applied: 31'), and 31 + 6 = 37 covers every open
|
|
88
|
+
// goal. A stale entry is harmless — planFor is idempotent and simply never matches.
|
|
89
|
+
//
|
|
90
|
+
// Pinned rather than derived on purpose: derived from the live set it would pass
|
|
91
|
+
// forever and never catch a NEW goal. That pin is what surfaced 1000080.
|
|
92
|
+
assert.equal(Object.keys(GOAL_NAMES).length + ALREADY_NAMES.length, 38);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('re-running is a no-op — planFor is idempotent', () => {
|
|
96
|
+
const want = GOAL_NAMES[1000027];
|
|
97
|
+
const already = { id: 1000027, title: want.title, subtitle: want.subtitle };
|
|
98
|
+
assert.deepEqual(planFor(already, want), {}, 'nothing to patch when already correct');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('planFor patches only what differs', () => {
|
|
102
|
+
const want = GOAL_NAMES[1000027];
|
|
103
|
+
const titleOnly = { id: 1, title: 'Findability — the grep path (ADR 0095)', subtitle: want.subtitle };
|
|
104
|
+
assert.deepEqual(Object.keys(planFor(titleOnly, want)), ['title']);
|
|
105
|
+
const subOnly = { id: 1, title: want.title, subtitle: '' };
|
|
106
|
+
assert.deepEqual(Object.keys(planFor(subOnly, want)), ['subtitle']);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('a NULL subtitle is treated as absent, not as a value', () => {
|
|
110
|
+
const want = GOAL_NAMES[1000027];
|
|
111
|
+
const fresh = { id: 1, title: want.title, subtitle: null };
|
|
112
|
+
assert.deepEqual(Object.keys(planFor(fresh, want)), ['subtitle'], 'null must still be filled in');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('the version catch-all is named for what it is', () => {
|
|
116
|
+
const v = GOAL_NAMES[1000003];
|
|
117
|
+
assert.ok(!/BONGOS-V1/.test(v.title), 'a version id is not a name');
|
|
118
|
+
assert.ok(!/general/i.test(v.title), '"general" names nothing');
|
|
119
|
+
assert.match(v.subtitle, /catch-all/i, 'and the subtitle says what it actually is');
|
|
120
|
+
});
|
package/tests/hall_nav.mjs
CHANGED
|
@@ -149,7 +149,13 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
149
149
|
// Gated cosmetically to match its data endpoint — `project.curate`, floor METIC,
|
|
150
150
|
// so the nav entry reads 'government' since ADR 0174 (it said 'archon', then
|
|
151
151
|
// was stricter than the wall). requireProjectCuratePage is the wall server-side.
|
|
152
|
-
|
|
152
|
+
// task 1003172 (C02 of goal 1000075): 'collab' joins Community, FIRST. The
|
|
153
|
+
// group is picked by whose data it is, and a help request is another builder
|
|
154
|
+
// reaching you — not your own numbers (that is You), and not reference (Docs).
|
|
155
|
+
// First within the group because it is the one page here you ACT on; the
|
|
156
|
+
// leaderboard and the directory are browsing surfaces. Updating this oracle is
|
|
157
|
+
// the reviewed decision the pin exists to force.
|
|
158
|
+
assert.deepEqual(nav[2].items.map((i) => i.id), ['collab', 'leaderboard', 'people'], 'Community = everyone else');
|
|
153
159
|
assert.deepEqual(nav[3].items.map((i) => i.id),
|
|
154
160
|
['primer', 'ranks', 'economy', 'diagrams', 'atlas', 'modules', 'apidocs'], 'Docs = reference material');
|
|
155
161
|
// 'government' (the Permissions tab, BV1.R113) is the group's one archon-gated item.
|