@bongos/core 1.19.344 → 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.344",
6
- "core_contract": "1.19.344",
7
- "source_commit": "8c08be71d310a5efcda6758c75e8483382426e06",
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-25T01:57:13.504Z",
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": "6cf7037df1f7ae7c712b6255408972f1e179b6487b592b76e87520344ed0756f",
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": "ce99df6084ae3c2b151cc254d147ece750898e9d87d33fb204b32ace1a7d8029"
4745
+ "sha256": "4f0ae4d95143c43bf5761820758130da48c40d0a2fde082d898fb6a6175ef853"
4746
4746
  },
4747
4747
  {
4748
4748
  "path": "package.json",
4749
4749
  "mode": "0000644",
4750
- "sha256": "cf51aa1815eea987d945024edc01d09faf17c7adf77ef381844d56ac096db7d9"
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": "26318bb3c73b91064f7f85d04c66203a16da0de847993823dadf3a340db7b175"
6600
+ "sha256": "ac7a592af3eb6de39e0a7f095d4a17bb89934aaa7064107fe0d462a0aaee4e2b"
6591
6601
  },
6592
6602
  {
6593
6603
  "path": "src/module-loader/catalog.js",
@@ -6847,7 +6857,7 @@
6847
6857
  {
6848
6858
  "path": "tests/baseline_staleness.mjs",
6849
6859
  "mode": "0000644",
6850
- "sha256": "44a2469b0f18c62fab6d9b3fe5a5125b8a9a351b5b1b48cb88cd812994d86b10"
6860
+ "sha256": "2543492e930ace22ed9de1bf69c6efb5fdba265e88d322a45a35da18248a8731"
6851
6861
  },
6852
6862
  {
6853
6863
  "path": "tests/bfg.mjs",
@@ -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.344",
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.344",
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.344",
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
@@ -1118,8 +1118,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1118
1118
  // landed since 1.19.342 with no explicit bump. run 32799245758. (task 1002620)
1119
1119
  // 1.19.344 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1120
1120
  // landed since 1.19.343 with no explicit bump. run 32799492873. (task 1002620)
1121
+ // 1.19.345 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
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)
1121
1125
  // ---------------------------------------------------------------------------
1122
- const CORE_VERSION = '1.19.344'; // 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
1123
1127
 
1124
1128
  // A namespaced logger so a module's log lines are attributable + consistent.
1125
1129
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -165,18 +165,27 @@ test('LIVE: every floor in the shipped baseline file carries a reason', () => {
165
165
  assert.equal(r.historyAvailable, true, 'this checkout should have git history for the baseline file');
166
166
  });
167
167
 
168
- test('LIVE: shrink the window to 1 day and the real baselines DO get flagged', () => {
169
- // Proof the check is not passing because it cannot see. Every current baseline
170
- // was written days ago, so at a one-day window the reducible ones must surface —
171
- // if this returns nothing, the detector is inert.
172
- const r = bs.run({ maxAgeDays: 1 });
173
- assert.ok(r.stale.length > 0, 'a 1-day window must flag the real baselines');
168
+ test('LIVE: shrink the window past zero and the real baselines DO get flagged', () => {
169
+ // Proof the detector is not passing merely because it cannot see. A negative
170
+ // window makes EVERY metric with a measurable age older than the threshold, so
171
+ // the flagged set is decided purely by the exemption rules.
172
+ //
173
+ // This deliberately names no individual metric. The first version asserted
174
+ // `doc_stale_links` would be flagged; task 1003211 then reduced that metric,
175
+ // resetting its age to zero, and the test went red on a tree that was perfectly
176
+ // healthy — it had been pinned to transient state rather than to a property.
177
+ // Paying debt down must never break the check that watches the debt.
178
+ const r = bs.run({ maxAgeDays: -1 });
174
179
  const names = r.stale.map((s) => s.name);
175
- assert.ok(names.includes('doc_stale_links'), `expected doc_stale_links among ${names.join(', ')}`);
176
- // …and the exempt ones must STILL be exempt at a 1-day window: the exemption is
177
- // the floor, not the clock.
180
+ assert.ok(names.length > 0, 'a negative window must flag every metric that is above its floor');
181
+ // The exemptions are the floor and zero, never the clock — so they hold even
182
+ // when the window guarantees everything else is flagged.
178
183
  assert.ok(!names.includes('require_cycle_count'), 'a metric at its declared floor must never be flagged');
179
184
  assert.ok(!names.includes('oversized_file_count'), 'a metric at zero must never be flagged');
185
+ assert.ok(!names.includes('excess_lines_total'), 'a metric at zero must never be flagged');
186
+ // Every flagged metric must genuinely be above its floor — the flag means
187
+ // "reducible and unmoved", not merely "old".
188
+ for (const s of r.stale) assert.ok(s.value > s.floor, `${s.name} flagged at or below its floor`);
180
189
  });
181
190
 
182
191
  test('LIVE: --update-baselines preserves the floors instead of overwriting them', () => {
@@ -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
+ });