@bongos/core 1.19.322 → 1.19.324

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.
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/version-literal-guard.js — no ops script names a version id
3
+ // (task 1003166 / G02, goal 1000077; criterion guards-no-hardcoded-version).
4
+ //
5
+ // THE INCIDENT. smoke-write.sh hardcoded 'PMS-V1'. That version shipped, so
6
+ // `version_closed` refused every create and the write-path smoke COULD NOT PASS
7
+ // — while /merge-mode Step 5 went on telling the operator "4/4 expected". The
8
+ // failure reads as a broken smoke, not a stale constant, which is why it sat
9
+ // (idea 1000823). Nine scripts carried the same bomb; four of the versions they
10
+ // named (PMS-V1, PMS-V2, GDS-V3, GDS-V4) had ALREADY shipped when this ran.
11
+ //
12
+ // WHAT IT CHECKS, narrowly: a version-shaped literal ASSIGNED TO A VERSION FIELD
13
+ // or used as a version DEFAULT, in an executable script. Not any mention of a
14
+ // version anywhere — smoke-claim-consistency.sh legitimately says "PMS-V2 P6 not
15
+ // live yet" in a comment and an echo, and flagging that would be the over-broad
16
+ // source assertion this repo has learned to avoid (learning 1000196). Comment
17
+ // lines are stripped before matching for the same reason.
18
+ //
19
+ // EXCLUDED BY PATH, not by allowlist: seeders and tests. A seeder's whole subject
20
+ // IS a specific historical version — `seed-v3-tasks.js` naming GDS-V3 is correct,
21
+ // and asking it to resolve one at runtime would break it.
22
+
23
+ 'use strict';
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ const { execFileSync } = require('node:child_process');
28
+
29
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
30
+
31
+ // Any prefix, so a version scheme invented next year is covered too — better
32
+ // than enumerating PMS/GDS/BONGOS/CB and finding out later that OTB-V2 slipped.
33
+ const VERSION_LITERAL = String.raw`['"][A-Z][A-Z0-9]*-V\d+['"]`;
34
+ // A version FIELD or a version DEFAULT — the two shapes that actually rot.
35
+ const VERSION_FIELD = String.raw`version_id|versionId|suggested_version|target_version|DEFAULT_VERSION|VERSION_ID`;
36
+ const ASSIGNED = new RegExp(`(?:${VERSION_FIELD})\\s*['"]?\\s*[:=][^,;)]*${VERSION_LITERAL}`);
37
+ const DEFAULTED = new RegExp(`--version[^\\n]*?\\|\\|\\s*${VERSION_LITERAL}`);
38
+
39
+ // path::reason. A justified entry, one line, like every other guard here.
40
+ const VERSION_LITERAL_ALLOW = new Map([
41
+ ['scripts/gds/grade-smoke.js',
42
+ 'GOOD_TASK / BAD_TASK are in-memory fixtures handed to grader.gradeSubagent; they are never POSTed, so no version_closed can refuse them and the literal cannot rot into a failure.'],
43
+ ['scripts/gds/status.js',
44
+ 'DEFAULT_VERSION is an owner-facing dashboard default with an env override, and TWO versions are currently building — so "most recent building" is not a derivable answer here. Retargeting what /status shows by default is an owner decision, not a refactor.'],
45
+ ]);
46
+
47
+ function isExempt(rel) {
48
+ return /(^|\/)seed-[^/]*\.js$/.test(rel) // a seeder's subject IS that version
49
+ || /\.test\.js$/.test(rel) // fixtures pinned to a historical version
50
+ || /(^|\/)fixtures\//.test(rel)
51
+ || /(^|\/)version-literal-guard\.js$/.test(rel); // this file names the shapes it hunts
52
+ }
53
+
54
+ // Drop comment lines only — never inline content, which is where real code lives.
55
+ function stripComments(src, rel) {
56
+ const lineComment = rel.endsWith('.sh') ? /^\s*#/ : /^\s*(\/\/|\*|\/\*)/;
57
+ return String(src).split('\n').map((l) => (lineComment.test(l) ? '' : l));
58
+ }
59
+
60
+ function scanScripts(files, read) {
61
+ const violations = [];
62
+ let scanned = 0;
63
+ for (const rel of files) {
64
+ if (isExempt(rel)) continue;
65
+ const src = read(rel);
66
+ if (src === null) continue;
67
+ scanned += 1;
68
+ if (VERSION_LITERAL_ALLOW.has(rel)) continue;
69
+ stripComments(src, rel).forEach((line, i) => {
70
+ if (ASSIGNED.test(line) || DEFAULTED.test(line)) {
71
+ violations.push(
72
+ `${rel}:${i + 1} names a version id where one should be RESOLVED: ${line.trim().slice(0, 90)}`
73
+ + ' — a literal works until that version ships, then version_closed refuses every write and the'
74
+ + ' failure reads as broken code. Use resolveDefaultVersion() from version-select.js'
75
+ + ' (bash: VERSION_ID=$(node scripts/gds/version-select.js)), or allowlist it with a reason.'
76
+ );
77
+ }
78
+ });
79
+ }
80
+ return { violations, scanned };
81
+ }
82
+
83
+ function trackedScripts() {
84
+ const out = execFileSync('git', ['ls-files', 'scripts/*.js', 'scripts/*.sh', 'scripts/**/*.js', 'scripts/**/*.sh'], {
85
+ cwd: REPO_ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
86
+ });
87
+ return String(out || '').split('\n').map((f) => f.trim()).filter(Boolean);
88
+ }
89
+
90
+ const NAME = 'no ops script hardcodes a version id (G02)';
91
+
92
+ function checkNoVersionLiterals({ files = null, read = null } = {}) {
93
+ const list = files || trackedScripts();
94
+ if (!list.length) {
95
+ return { name: NAME, ok: false, hardFail: true,
96
+ violations: ['scan defect — enumerated 0 tracked scripts; a broken enumeration, not a clean result.'],
97
+ warnings: [], note: 'static scan of scripts/ for hardcoded version ids.' };
98
+ }
99
+ const reader = read || ((rel) => {
100
+ try { return fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8'); } catch { return null; }
101
+ });
102
+ const { violations, scanned } = scanScripts(list, reader);
103
+ return {
104
+ name: NAME,
105
+ ok: violations.length === 0,
106
+ hardFail: violations.length > 0,
107
+ violations,
108
+ warnings: [],
109
+ note: `${scanned} executable script(s) under scripts/ scanned: none may assign a version-shaped literal to a version field or use one as a --version default. Seeders, *.test.js and fixtures are exempt by path (their subject IS a historical version); ${VERSION_LITERAL_ALLOW.size} justified allowlist entr${VERSION_LITERAL_ALLOW.size === 1 ? 'y' : 'ies'}.`,
110
+ };
111
+ }
112
+
113
+ function main() {
114
+ const r = checkNoVersionLiterals();
115
+ for (const v of r.violations) console.error(`✗ ${v}`);
116
+ console.log(`${r.hardFail ? 'FAIL' : 'PASS'} ${r.note}`);
117
+ process.exit(r.hardFail ? 1 : 0);
118
+ }
119
+
120
+ if (require.main === module) main();
121
+
122
+ module.exports = { checkNoVersionLiterals, scanScripts, stripComments, isExempt, VERSION_LITERAL_ALLOW, ASSIGNED, DEFAULTED };
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/version-select.js — resolve "the version to use" at RUNTIME
3
+ // (task 1003166 / G02, goal 1000077; criterion guards-no-hardcoded-version).
4
+ //
5
+ // THE BUG THIS ENDS. scripts/gds/smoke-write.sh hardcoded 'PMS-V1'. That version
6
+ // shipped, so `version_closed` correctly refuses the create — meaning the
7
+ // write-path smoke CANNOT pass — while /merge-mode Step 5 still tells the
8
+ // operator "4/4 expected". A literal version id is a time bomb with a long fuse:
9
+ // it works until the version ships, and then it fails in a way that reads as a
10
+ // broken smoke rather than a stale constant (idea 1000823).
11
+ //
12
+ // Nine scripts carried one. The rule they now share is capture.js's, which was
13
+ // already right: the most recent BUILDING version. Extracted here so there is one
14
+ // implementation rather than nine, and exposed as a CLI so the bash smokes get
15
+ // the same answer as the JS callers instead of reimplementing the sort in python.
16
+ //
17
+ // const { resolveDefaultVersion } = require('./version-select.js'); // JS
18
+ // VERSION_ID=$(node scripts/gds/version-select.js) || exit 1 # bash
19
+
20
+ 'use strict';
21
+
22
+ const { cliExit } = require('./cli-lib');
23
+
24
+ // PURE, so the selection rule is unit-tested without a session or a network.
25
+ // Returns a version id, or null when nothing is building — never a guess.
26
+ function pickDefaultVersion(versions) {
27
+ if (!Array.isArray(versions)) return null;
28
+ const building = versions.filter((v) => v && v.status === 'building');
29
+ if (building.length === 0) return null;
30
+ // Most recent by started_at. A null started_at sorts last rather than
31
+ // throwing, so a half-filled row cannot decide the answer (capture.js's
32
+ // original behaviour, kept deliberately).
33
+ building.sort((a, b) => {
34
+ const ta = a.started_at ? new Date(a.started_at).getTime() : 0;
35
+ const tb = b.started_at ? new Date(b.started_at).getTime() : 0;
36
+ return tb - ta;
37
+ });
38
+ return building[0].id;
39
+ }
40
+
41
+ async function resolveDefaultVersion(deps = {}) {
42
+ const client = deps.cliClient || require('./cli-lib').cliClient;
43
+ const api = await client();
44
+ const r = await api.versions.getVersions();
45
+ if (!r.ok) return null;
46
+ return pickDefaultVersion(r.data?.versions ?? []);
47
+ }
48
+
49
+ // A version id is interpolated by bash callers into a quoted Python string, so
50
+ // the ONE place that can guarantee it is safe is here, at the source, rather than
51
+ // four escaping fixes downstream. Ids are `[A-Za-z][A-Za-z0-9._-]*` in practice;
52
+ // anything else is refused rather than printed. Defense in depth: writing a
53
+ // versions.id is already a privileged operation.
54
+ const SAFE_ID = /^[A-Za-z][A-Za-z0-9._-]*$/;
55
+
56
+ async function main() {
57
+ const id = await resolveDefaultVersion();
58
+ if (!id) {
59
+ // Exit non-zero rather than printing something a caller would interpolate:
60
+ // a smoke that creates a task against the empty string is worse than one
61
+ // that stops and says why.
62
+ console.error('version-select: no version is currently `building` — cannot resolve a default.');
63
+ console.error(' Open one, or pass an explicit --version to the caller.');
64
+ return cliExit(1);
65
+ }
66
+ if (!SAFE_ID.test(id)) {
67
+ console.error(`version-select: refusing to emit ${JSON.stringify(id)} — not a plain version id.`);
68
+ console.error(' Callers splice this into a quoted string; an id with a quote or backslash could break out.');
69
+ return cliExit(1);
70
+ }
71
+ process.stdout.write(id);
72
+ return cliExit(0);
73
+ }
74
+
75
+ if (require.main === module) {
76
+ main().catch((err) => { console.error(`version-select: ${err && err.message}`); cliExit(1); });
77
+ }
78
+
79
+ module.exports = { pickDefaultVersion, resolveDefaultVersion, SAFE_ID };
package/src/module-api.js CHANGED
@@ -1071,8 +1071,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1071
1071
  // landed since 1.19.320 with no explicit bump. run 32679324013. (task 1002620)
1072
1072
  // 1.19.322 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1073
1073
  // landed since 1.19.321 with no explicit bump. run 32679604360. (task 1002620)
1074
+ // 1.19.323 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1075
+ // landed since 1.19.322 with no explicit bump. run 32680138776. (task 1002620)
1076
+ // 1.19.324 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1077
+ // landed since 1.19.323 with no explicit bump. run 32737868129. (task 1002620)
1074
1078
  // ---------------------------------------------------------------------------
1075
- const CORE_VERSION = '1.19.322'; // CI auto-patch carrier (ADR 0161); see changelog above
1079
+ const CORE_VERSION = '1.19.324'; // CI auto-patch carrier (ADR 0161); see changelog above
1076
1080
 
1077
1081
  // A namespaced logger so a module's log lines are attributable + consistent.
1078
1082
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,237 @@
1
+ // tests/goal_subtitle.mjs — a goal's explanation stops living inside its name
2
+ // (task 1003224, goal 1000072, criterion legibility-goal-names-and-claude-text).
3
+ //
4
+ // Owner directive, three parts: the TITLE is a name; the SUBTITLE is a separate,
5
+ // subordinate, on-demand field; and the bulk prose Claude writes into the
6
+ // description is presented as Claude's text, absent from the default reading
7
+ // path until someone asks for it.
8
+ //
9
+ // These tests pin the parts a future edit could quietly undo: that subtitle is
10
+ // carried by the same prose-edit path as title (so it inherits the audit trail
11
+ // rather than getting a private one), that the disclosure is CLOSED by default,
12
+ // and that task creation did NOT gain a subtitle — the identical validator pair
13
+ // appears in POST /goals and POST /goals/:id/tasks, and only the first should
14
+ // have changed.
15
+ import { strict as assert } from 'node:assert';
16
+ import { test } from 'node:test';
17
+ import { createRequire } from 'node:module';
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
24
+ const goalEdits = require(path.join(ROOT, 'modules', 'lifecycle', 'goal-edits.js'));
25
+ const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8');
26
+
27
+ const fakeReqRes = ({ body }) => {
28
+ const res = { code: null, statusCode: null, payload: null,
29
+ fail(code, status, extra) { this.code = code; this.statusCode = typeof status === 'number' ? status : status?.status ?? 400; this.payload = { error: code, ...(extra || {}) }; return this; },
30
+ status(c) { this.statusCode = c; return this; },
31
+ json(p) { this.payload = p; return this; } };
32
+ return { req: { params: { id: '7' }, body, builder: { id: 1, rank: 'archon' } }, res };
33
+ };
34
+ const deps = {
35
+ db: { getGoal: async () => ({ id: 7, status: 'open', created_by: 1 }), getGoalMember: async () => null },
36
+ authorizeGoalManagement: () => ({ ok: true }),
37
+ validateOrRespond: () => false,
38
+ parseId: () => 7,
39
+ LIMITS: { TITLE: 200, DESCRIPTION: 50000 },
40
+ };
41
+
42
+ // ---- the field travels the existing prose path -----------------------------
43
+ test('a subtitle-only PATCH is accepted and recorded', () => {
44
+ // Not a private write path: subtitle rides title's handler so the prose-edit
45
+ // audit trail (deltas + record) covers it identically.
46
+ let patched = null; const recorded = [];
47
+ const handler = goalEdits.goalProseHandler({
48
+ ...deps,
49
+ update: async ({ patch }) => { patched = patch; return { goal: { id: 7, ...patch }, deltas: [goalEdits.proseDelta('subtitle', null, patch.subtitle)] }; },
50
+ record: async (...a) => { recorded.push(a); },
51
+ });
52
+ const { req, res } = fakeReqRes({ body: { subtitle: 'the grep path' } });
53
+ return handler(req, res).then(() => {
54
+ assert.equal(res.code, null, `must not fail — got ${res.code}`);
55
+ assert.deepEqual(patched, { subtitle: 'the grep path' }, 'only the named field is patched');
56
+ assert.equal(res.payload.deltas[0].field, 'subtitle');
57
+ assert.equal(recorded.length, 1, 'the edit is audit-recorded like any other prose edit');
58
+ });
59
+ });
60
+
61
+ test('an EMPTY subtitle is a legal value, not a no-op', () => {
62
+ // '' means deliberately blank; NULL means never set. The column has no
63
+ // default precisely so the UI can tell them apart, so '' must reach the patch.
64
+ let patched = null;
65
+ const handler = goalEdits.goalProseHandler({
66
+ ...deps,
67
+ update: async ({ patch }) => { patched = patch; return { goal: { id: 7 }, deltas: [] }; },
68
+ record: async () => {},
69
+ });
70
+ const { req, res } = fakeReqRes({ body: { subtitle: '' } });
71
+ return handler(req, res).then(() => {
72
+ assert.equal(res.code, null);
73
+ assert.ok(Object.prototype.hasOwnProperty.call(patched, 'subtitle'), 'empty string must still be a patch');
74
+ assert.equal(patched.subtitle, '');
75
+ });
76
+ });
77
+
78
+ test('nothing_to_update names subtitle as editable', () => {
79
+ const handler = goalEdits.goalProseHandler(deps);
80
+ const { req, res } = fakeReqRes({ body: { nope: 1 } });
81
+ return handler(req, res).then(() => {
82
+ assert.equal(res.code, 'nothing_to_update');
83
+ assert.deepEqual(res.payload.editable, ['title', 'subtitle', 'description']);
84
+ });
85
+ });
86
+
87
+ test('title and description still work — subtitle is additive', () => {
88
+ let patched = null;
89
+ const handler = goalEdits.goalProseHandler({
90
+ ...deps,
91
+ update: async ({ patch }) => { patched = patch; return { goal: { id: 7, ...patch }, deltas: [] }; },
92
+ record: async () => {},
93
+ });
94
+ const { req, res } = fakeReqRes({ body: { title: 'Findability', description: 'long prose' } });
95
+ return handler(req, res).then(() => {
96
+ assert.deepEqual(patched, { title: 'Findability', description: 'long prose' });
97
+ assert.ok(!Object.prototype.hasOwnProperty.call(patched, 'subtitle'), 'an unsent field is not patched to null');
98
+ });
99
+ });
100
+
101
+ // ---- the data layer --------------------------------------------------------
102
+ test('the migration is additive, core-numbered, and sets no default', () => {
103
+ const raw = read('migrations', 'core_224_goal_subtitle.sql');
104
+ assert.match(raw, /ADD COLUMN IF NOT EXISTS subtitle text/);
105
+ // Assert over the STATEMENTS, not the prose. The first version of this test
106
+ // matched /DEFAULT ''/ anywhere in the file and failed on the migration's own
107
+ // comment explaining why it does NOT set a default — the same over-broad-source
108
+ // -assertion mistake this session has made repeatedly (learning 1000196).
109
+ const ddl = raw.split('\n').filter((l) => !/^\s*--/.test(l)).join('\n');
110
+ assert.ok(!/\bDROP\b/i.test(ddl), 'no destructive DDL');
111
+ assert.ok(!/SET NOT NULL/i.test(ddl), 'stays nullable');
112
+ assert.ok(!/ADD COLUMN[^;]*DEFAULT/i.test(ddl), 'the added column takes no default');
113
+ assert.match(ddl, /schema_migrations/, 'records itself');
114
+ });
115
+
116
+ test('subtitle is in GOAL_COLS, so every read returns it', () => {
117
+ // One column list drives getGoal, the list query and createGoal's RETURNING —
118
+ // adding it anywhere else would be a second source that can drift.
119
+ const src = read('modules', 'lifecycle', 'db-goals.js');
120
+ const cols = /const GOAL_COLS\s*=\s*\n?\s*'([^']+)'/.exec(src);
121
+ assert.ok(cols, 'GOAL_COLS is still a single string literal');
122
+ assert.ok(cols[1].split(',').map((c) => c.trim()).includes('subtitle'), 'subtitle is selected');
123
+ });
124
+
125
+ test('POST /goals gained subtitle and POST /goals/:id/tasks did NOT', () => {
126
+ // The identical `title` + `description` validator pair appears in both routes.
127
+ // A blind replace-all would have given task creation a subtitle; this pins the
128
+ // boundary. (The first attempt at this edit tripped on exactly that ambiguity.)
129
+ const src = read('modules', 'lifecycle', 'routes', 'goals.js');
130
+ const goalRoute = src.slice(src.indexOf("router.post('/goals',"), src.indexOf("router.get('/goals',"));
131
+ assert.match(goalRoute, /subtitle: \{ type: 'string'/, 'the goal route takes a subtitle');
132
+ const taskRoute = src.slice(src.indexOf("router.post('/goals/:id/tasks',"));
133
+ const taskValidator = taskRoute.slice(0, taskRoute.indexOf('})) return;'));
134
+ assert.ok(!/subtitle/.test(taskValidator), 'task creation must NOT accept a subtitle');
135
+ });
136
+
137
+ // ---- the default reading path ---------------------------------------------
138
+ test("Claude's prose is a CLOSED disclosure, not visible body text", () => {
139
+ const html = read('modules', 'hall-ui', 'public', 'goals.html');
140
+ assert.match(html, /<details class="claude-text"[^>]*hidden/, 'a <details>, hidden until it has content');
141
+ assert.ok(!/<p class="goal-detail__desc" id="goal-detail-desc"><\/p>/.test(html),
142
+ 'the description is no longer a bare always-visible paragraph');
143
+ // the description element must now live INSIDE the disclosure
144
+ const det = html.slice(html.indexOf('<details class="claude-text"'));
145
+ const inner = det.slice(0, det.indexOf('</details>'));
146
+ assert.match(inner, /id="goal-detail-desc"/, 'the description renders inside the disclosure');
147
+ });
148
+
149
+ // applyGoalProse is pure enough to RUN, so these two assert behaviour rather
150
+ // than source text — a stronger pin, and the reason the rules were moved out of
151
+ // goals-page.js (which is 500 lines over the excess-lines ratchet) in the first
152
+ // place. Fake elements stand in for the DOM.
153
+ const el = () => ({ textContent: null, hidden: null, open: null });
154
+ function applyProse(goal) {
155
+ const lib = read('modules', 'hall-ui', 'public', 'goals-lib.js');
156
+ const els = { sub: el(), desc: el(), claude: el() };
157
+ const fn = new Function('window', `${lib}; return window.OTBGoalsLib.applyGoalProse;`)({ OTB: { escapeHtml: (x) => x } });
158
+ fn(els, goal);
159
+ return els;
160
+ }
161
+
162
+ test('the disclosure is CLOSED on render, never opened for the user', () => {
163
+ const els = applyProse({ description: 'Claude wrote a lot here', subtitle: 'sub' });
164
+ assert.equal(els.claude.open, false, 'closed');
165
+ assert.equal(els.claude.hidden, false, 'present, because there is prose to reveal');
166
+ assert.equal(els.desc.textContent, 'Claude wrote a lot here');
167
+ });
168
+
169
+ test('a goal with no description shows no empty disclosure', () => {
170
+ const els = applyProse({ description: ' ', subtitle: 'sub' });
171
+ assert.equal(els.claude.hidden, true, 'an empty disclosure invites a dead click');
172
+ });
173
+
174
+ test('an unset subtitle is hidden; a set one is shown trimmed', () => {
175
+ assert.equal(applyProse({}).sub.hidden, true, 'NULL subtitle renders nothing');
176
+ const els = applyProse({ subtitle: ' the grep path ' });
177
+ assert.equal(els.sub.hidden, false);
178
+ assert.equal(els.sub.textContent, 'the grep path');
179
+ });
180
+
181
+ test('on a LIST the subtitle is a tooltip, never a second visible line', () => {
182
+ const js = read('modules', 'hall-ui', 'public', 'goals-page.js');
183
+ // The helper lives in goals-lib.js — the file that exists because
184
+ // goals-page.js is over the excess-lines ratchet (task 1003135).
185
+ const lib = read('modules', 'hall-ui', 'public', 'goals-lib.js');
186
+ assert.match(lib, /function subtitleAttr/);
187
+ assert.match(lib, /escapeHtml\(t\)/, 'escaped, since it lands in an attribute');
188
+ for (const anchor of ['class="gr-title"', 'class="goal-card__title gr-title"']) {
189
+ const i = js.indexOf(anchor);
190
+ assert.ok(i > 0, `${anchor} still rendered`);
191
+ assert.match(js.slice(i, i + 160), /subtitleAttr\(g\)/, `${anchor} carries the tooltip`);
192
+ }
193
+ });
194
+
195
+ test('every CSS token the new styles use actually exists', () => {
196
+ // copy-desk.css shipped with --line and --panel, which style.css never
197
+ // defines, so its fallbacks rendered as nothing (idea 1000821).
198
+ const css = read('modules', 'hall-ui', 'public', 'goals.css');
199
+ const style = read('modules', 'hall-ui', 'public', 'style.css');
200
+ const block = css.slice(css.indexOf('task 1003224'));
201
+ const used = new Set([...block.matchAll(/var\((--[a-z-]+)\)/g)].map((m) => m[1]));
202
+ assert.ok(used.size >= 4, 'the new block does use tokens');
203
+ for (const t of used) {
204
+ assert.ok(new RegExp(`^\\s*${t}:`, 'm').test(style), `${t} must be defined in style.css`);
205
+ }
206
+ });
207
+
208
+ test('the subtitle style is subordinate to the title style', () => {
209
+ const css = read('modules', 'hall-ui', 'public', 'goals.css');
210
+ const size = (sel) => {
211
+ const i = css.indexOf(sel);
212
+ assert.ok(i > 0, `${sel} exists`);
213
+ const m = /font-size:\s*([\d.]+)px/.exec(css.slice(i, i + 300));
214
+ return m ? Number(m[1]) : null;
215
+ };
216
+ const title = size('.goal-detail__title');
217
+ const sub = size('.goal-detail__subtitle');
218
+ assert.ok(title && sub, 'both declare a size');
219
+ assert.ok(sub < title, `subtitle (${sub}px) must read smaller than the name (${title}px)`);
220
+ });
221
+
222
+ test("the PATCH route's own comment names every field it accepts", () => {
223
+ // The API reference is GENERATED from this comment, so the generator
224
+ // faithfully republishes a stale one: after subtitle was added the docs still
225
+ // advertised 'title/description', which the grader caught as a blocker. This
226
+ // pins comment and validator together so the next added field cannot drift.
227
+ const routes = read('modules', 'lifecycle', 'routes', 'goals.js');
228
+ const i = routes.indexOf("router.patch('/goals/:id',");
229
+ assert.ok(i > 0, 'the PATCH route still exists');
230
+ const comment = routes.slice(routes.lastIndexOf('// PATCH /goals/:id', i), i);
231
+ const edits = read('modules', 'lifecycle', 'goal-edits.js');
232
+ const validator = edits.slice(edits.indexOf('function goalProseHandler'));
233
+ for (const field of ['title', 'subtitle', 'description']) {
234
+ assert.match(validator, new RegExp(`${field}: \\{ `), `${field} is validated`);
235
+ assert.ok(comment.includes(field), `the route comment must name ${field} — the API reference is generated from it`);
236
+ }
237
+ });
@@ -65,8 +65,10 @@ await test('createGoal: INSERT binds all six fields in order and returns the row
65
65
  );
66
66
  assert.deepEqual(out, GOAL_ROW, 'returns the inserted row');
67
67
  const ins = find(pool.queries, /INSERT INTO goals/);
68
- assert.deepEqual(ins.params, ['BONGOS-V1', 'Goal hierarchy', 'desc', ['gds-core'], 'open', 42],
69
- 'version_id, title, description, scope_modules, status, created_by — in that order');
68
+ // task 1003224 appended `subtitle` as $7 rather than inserting it next to
69
+ // title, so $1–$6 keep the positions this test has always pinned.
70
+ assert.deepEqual(ins.params, ['BONGOS-V1', 'Goal hierarchy', 'desc', ['gds-core'], 'open', 42, null],
71
+ 'version_id, title, description, scope_modules, status, created_by, then subtitle — in that order');
70
72
  assert.match(ins.sql, /RETURNING/, 'returns the row, not just inserts');
71
73
  });
72
74
 
@@ -87,8 +89,8 @@ await test('createGoal: defaults — null description/createdBy, empty scope, op
87
89
  const pool = fakePool((s) => (/INSERT INTO goals/.test(s) ? { rows: [GOAL_ROW] } : null));
88
90
  await createGoal({ versionId: 'V1', title: 'general' }, { pool });
89
91
  const ins = find(pool.queries, /INSERT INTO goals/);
90
- assert.deepEqual(ins.params, ['V1', 'general', null, [], 'open', null],
91
- 'unspecified fields fall back to null / [] / open');
92
+ assert.deepEqual(ins.params, ['V1', 'general', null, [], 'open', null, null],
93
+ 'unspecified fields fall back to null / [] / open — subtitle ($7) defaults to null too');
92
94
  });
93
95
 
94
96
  // =========================================================================