@bongos/core 1.19.345 → 1.19.346

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 CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.345",
6
- "core_contract": "1.19.345",
7
- "source_commit": "ef6d8227178f4b2a0b050772442f2440b6ad34ab",
5
+ "core_version": "1.19.346",
6
+ "core_contract": "1.19.346",
7
+ "source_commit": "17c6cd8e45c6d2f44d50888c8bcd9c5daa0889f3",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-25T02:17:29.630Z",
9
+ "built_at": "2026-08-25T02:22:50.262Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 328,
13
13
  "agent_docs_stubbed": 22,
14
- "functional_verbatim": 1489,
14
+ "functional_verbatim": 1492,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1839,
20
- "tree_sha256": "2405d3d77e5a22dee9b903e443cef2f24c9b91b96c890cd2ad93b26ed7e208d4",
19
+ "file_count": 1842,
20
+ "tree_sha256": "88d683cf1954503b8c2d4555474808840900d1b16471c26c626c53f076927df5",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -4742,12 +4742,12 @@
4742
4742
  {
4743
4743
  "path": "package-lock.json",
4744
4744
  "mode": "0000644",
4745
- "sha256": "1f08aad9924b7023db8de3c1bb3b7212da7a826227695318d23634865f729975"
4745
+ "sha256": "4f0ae4d95143c43bf5761820758130da48c40d0a2fde082d898fb6a6175ef853"
4746
4746
  },
4747
4747
  {
4748
4748
  "path": "package.json",
4749
4749
  "mode": "0000644",
4750
- "sha256": "1790b798df1626b11f335c995d9c6e6e6c297fead47852cab15f8fcfae4f89f0"
4750
+ "sha256": "9ecdcc1f7c105d33b249c6953d18a6c8769d6fa767b996333a456f9b32ad141c"
4751
4751
  },
4752
4752
  {
4753
4753
  "path": "public-docs/index.html",
@@ -5509,6 +5509,11 @@
5509
5509
  "mode": "0000644",
5510
5510
  "sha256": "1e14a452c0ae53384facd6a20526a90106cb479c414937ebb7fe180f11c5fa31"
5511
5511
  },
5512
+ {
5513
+ "path": "scripts/gds/goal-names.js",
5514
+ "mode": "0000644",
5515
+ "sha256": "5a2e40573f92007c4257c497b291f168878421c60423584d4c55775cc3aaf836"
5516
+ },
5512
5517
  {
5513
5518
  "path": "scripts/gds/goal.js",
5514
5519
  "mode": "0000644",
@@ -5809,6 +5814,11 @@
5809
5814
  "mode": "0000644",
5810
5815
  "sha256": "e4bd536cc2b91d2036d75ab67d53da1b67aee126e174da9dd8ac9531ae313b17"
5811
5816
  },
5817
+ {
5818
+ "path": "scripts/gds/rename-goals.js",
5819
+ "mode": "0000644",
5820
+ "sha256": "4cbb7baf533e1aa2eced1c828f37bbd11f7183846a0f3862f9fb72b3f504be66"
5821
+ },
5812
5822
  {
5813
5823
  "path": "scripts/gds/render-ideas.js",
5814
5824
  "mode": "0000644",
@@ -6587,7 +6597,7 @@
6587
6597
  {
6588
6598
  "path": "src/module-api.js",
6589
6599
  "mode": "0000644",
6590
- "sha256": "3aa07baacc3e1cdc43bc310db5e0c104c931117a788879501694c9d525083634"
6600
+ "sha256": "ac7a592af3eb6de39e0a7f095d4a17bb89934aaa7064107fe0d462a0aaee4e2b"
6591
6601
  },
6592
6602
  {
6593
6603
  "path": "src/module-loader/catalog.js",
@@ -7554,6 +7564,11 @@
7554
7564
  "mode": "0000644",
7555
7565
  "sha256": "bd29e6910bc741ddf18b4592eeeaeac02b45fb1dd321527a8f44203cd78bff18"
7556
7566
  },
7567
+ {
7568
+ "path": "tests/goal_names.mjs",
7569
+ "mode": "0000644",
7570
+ "sha256": "c93422d76ff7f0107190c6de1954f110e92a18913fabbcdea2b4f67693d538e6"
7571
+ },
7557
7572
  {
7558
7573
  "path": "tests/goal_routes.mjs",
7559
7574
  "mode": "0000644",
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.345",
3
+ "version": "1.19.346",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.345",
9
+ "version": "1.19.346",
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.345",
3
+ "version": "1.19.346",
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 };
package/src/module-api.js CHANGED
@@ -1120,8 +1120,10 @@ 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)
1123
1125
  // ---------------------------------------------------------------------------
1124
- const CORE_VERSION = '1.19.345'; // CI auto-patch carrier (ADR 0161); see changelog above
1126
+ const CORE_VERSION = '1.19.346'; // CI auto-patch carrier (ADR 0161); see changelog above
1125
1127
 
1126
1128
  // A namespaced logger so a module's log lines are attributable + consistent.
1127
1129
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -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
+ });