@bongos/core 1.19.459 → 1.19.460

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.
@@ -1,12 +1,12 @@
1
1
  {
2
- "_": "Copy desk — the one-voice review queue. Its confirm dialog is a real <dialog>, so its ::backdrop is the sheet's single sanctioned literal (black with alpha as a scrim). Written with the re-skin (task 1003307) so this page is MEASURED at three widths in both modes rather than looked at once.",
2
+ "_": "Copy desk — the one-voice review queue. Its two dialogs are real <dialog>s, so their ::backdrop is the sheet's single sanctioned literal (black with alpha as a scrim). Written with the re-skin (task 1003307) so this page is MEASURED at three widths in both modes rather than looked at once; the redesign (task 1003471) put a queue, ten duplicates and a search's hits in the harness so the rows and both dialogs can be looked at.",
3
3
  "page": "/builders/copy-desk",
4
4
  "surface": "hall-ui",
5
5
  "stub": {
6
6
  "prefix": "/builders/"
7
7
  },
8
8
  "modeQuery": false,
9
- "_feeds": "The kit's own stub carries no feed for this page, so against it the page renders its EMPTY and SKELETON states — which is the point: those are states the floors must hold in. Against the hall-preview harness the same states render populated. Every declared 404 below is a fixture the harness does not carry, named rather than hidden.",
9
+ "_feeds": "The kit's own stub carries no feed for this page, so against it the page renders its EMPTY state — which is the point: that is a state the floors must hold in. Against the hall-preview harness the same states render populated: copy-desk__queue.json (six flags across open, resolved and dismissed, with a gone target, a moved one and a whole-surface one; ten real duplicates) and copy-desk__strings.json (a search's hits). Every declared 404 below is a feed the kit's stub does not carry, named rather than hidden. The composer and closer states use clickIfVisible because against the stub the buttons do not exist.",
10
10
  "ignoreRequests": [
11
11
  "/api/(gds|bongos)/me$",
12
12
  "/api/(gds|bongos)/me/",
@@ -45,6 +45,63 @@
45
45
  },
46
46
  "out": {
47
47
  "auth": false
48
+ },
49
+ "search": {
50
+ "auth": true,
51
+ "actions": [
52
+ [
53
+ "wait",
54
+ 700
55
+ ],
56
+ [
57
+ "fill",
58
+ "#cd-search",
59
+ "save"
60
+ ],
61
+ [
62
+ "wait",
63
+ 600
64
+ ]
65
+ ],
66
+ "expect": {
67
+ "visible": [
68
+ "#cd-results"
69
+ ]
70
+ }
71
+ },
72
+ "composer": {
73
+ "auth": true,
74
+ "actions": [
75
+ [
76
+ "wait",
77
+ 700
78
+ ],
79
+ [
80
+ "clickIfVisible",
81
+ ".cd-flag-btn"
82
+ ],
83
+ [
84
+ "wait",
85
+ 250
86
+ ]
87
+ ]
88
+ },
89
+ "closer": {
90
+ "auth": true,
91
+ "actions": [
92
+ [
93
+ "wait",
94
+ 700
95
+ ],
96
+ [
97
+ "clickIfVisible",
98
+ ".cd-close-btn"
99
+ ],
100
+ [
101
+ "wait",
102
+ 250
103
+ ]
104
+ ]
48
105
  }
49
106
  },
50
107
  "reducedMotion": {
@@ -1,9 +1,12 @@
1
1
  /* oversight.css — the oversight family's archetype (task 1003469, goal
2
- 1000081): what a oversight ROW is in the chrome world, and the pieces the
2
+ 1000081): what an oversight ROW is in the chrome world, and the pieces the
3
3
  four pages (government, watch, harbor, gate) share around it — the figure
4
4
  band inside a panel, the group head, the inline form, the grant toggle, the
5
5
  confirm modal. The third family sheet after record.css and room.css: a page
6
- sheet holds only what one page has and the others do not.
6
+ sheet holds only what one page has and the others do not. The collaboration
7
+ pages (collab, copy desk — task 1003471) ride the same row rather than a
8
+ fourth: a page whose repeated thing is a titled row with facts and actions
9
+ links this sheet before its own.
7
10
 
8
11
  Every panel is the shell's .scroll; every heading its .scroll__h; every state
9
12
  pill its .fact-pill; every table its .ledger. This sheet adds nothing the
@@ -150,6 +153,12 @@ button.ov-row[aria-pressed="true"] .ov-row__title { color: var(--accent-ink); }
150
153
  The shell's glass .stat-row, given the air a panel would have under it. */
151
154
  .ov-summary { margin: 0 0 var(--sp-md); }
152
155
 
156
+ /* A chip that is a door (the task or goal a row is about) keeps the chip's
157
+ faint ink at rest — a coloured chip would read as a second status — and
158
+ takes the link ink on hover, the way the row's title link does. */
159
+ a.fact-chip { color: var(--ink-faint); }
160
+ a.fact-chip:hover { color: var(--accent-ink); border-color: var(--accent-ink); text-decoration: none; }
161
+
153
162
  /* ---- plain facts and the machine's words ------------------------------------ */
154
163
  .ov-fact { font-size: 12.5px; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
155
164
  .ov-fact b { color: var(--ink-soft); font-weight: 500; }
@@ -49,6 +49,16 @@ async function craftsForBuilder(builderId, deps = {}) {
49
49
  }
50
50
 
51
51
  const COLS = 'id, requested_by, needs_craft, needs_builder_id, what_is_stuck, task_id, goal_id, status, created_at, updated_at';
52
+ // The two list reads also NAME the people on a row — who is asking (the fact
53
+ // the collab page owes a helper: "who to answer", task 1003471) and who a
54
+ // by-name ask is addressed to — through two LEFT JOINs on builders. COLS stays
55
+ // bare for the writes, whose RETURNING cannot reach a joined alias.
56
+ const LIST_COLS = `${COLS.split(', ').map((c) => `h.${c}`).join(', ')}, `
57
+ + 'a.github_login AS requested_by_login, a.display_name AS requested_by_name, '
58
+ + 'n.github_login AS needs_builder_login, n.display_name AS needs_builder_name';
59
+ const LIST_FROM = 'FROM lifecycle_help_requests h '
60
+ + 'LEFT JOIN builders a ON a.id = h.requested_by '
61
+ + 'LEFT JOIN builders n ON n.id = h.needs_builder_id';
52
62
  const MAX_STUCK = 4000;
53
63
  // Defence in depth: both reads are bounded today (open-status churn, per-builder
54
64
  // scope), but an unsettled pile should degrade the page, not the server.
@@ -130,11 +140,11 @@ async function listOpenForBuilder({ builderId, crafts = [], limit = MAX_ROWS },
130
140
  const activePool = deps.pool || pool;
131
141
  const safeCrafts = (Array.isArray(crafts) ? crafts : []).filter((c) => HELP_CRAFTS.includes(c));
132
142
  const { rows } = await activePool.query(
133
- `SELECT ${COLS} FROM lifecycle_help_requests
134
- WHERE status = 'open'
135
- AND requested_by <> $1
136
- AND (needs_builder_id = $1 OR needs_craft = ANY($2::text[]))
137
- ORDER BY created_at DESC, id DESC
143
+ `SELECT ${LIST_COLS} ${LIST_FROM}
144
+ WHERE h.status = 'open'
145
+ AND h.requested_by <> $1
146
+ AND (h.needs_builder_id = $1 OR h.needs_craft = ANY($2::text[]))
147
+ ORDER BY h.created_at DESC, h.id DESC
138
148
  LIMIT $3`,
139
149
  [builderId, safeCrafts, capRows(limit)]
140
150
  );
@@ -181,9 +191,9 @@ function craftMapSql(col) {
181
191
  async function listMine({ builderId, status = null, limit = MAX_ROWS }, deps = {}) {
182
192
  const activePool = deps.pool || pool;
183
193
  const { rows } = await activePool.query(
184
- `SELECT ${COLS} FROM lifecycle_help_requests
185
- WHERE requested_by = $1 AND ($2::text IS NULL OR status = $2)
186
- ORDER BY created_at DESC, id DESC
194
+ `SELECT ${LIST_COLS} ${LIST_FROM}
195
+ WHERE h.requested_by = $1 AND ($2::text IS NULL OR h.status = $2)
196
+ ORDER BY h.created_at DESC, h.id DESC
187
197
  LIMIT $3`,
188
198
  [builderId, status, capRows(limit)]
189
199
  );
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.459",
3
+ "version": "1.19.460",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.459",
9
+ "version": "1.19.460",
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.459",
3
+ "version": "1.19.460",
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",
package/src/module-api.js CHANGED
@@ -33,7 +33,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
33
33
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
34
34
  // the entry to that file. Look for a version's history there, not here.
35
35
  // ---------------------------------------------------------------------------
36
- const CORE_VERSION = '1.19.459'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
36
+ const CORE_VERSION = '1.19.460'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
37
37
 
38
38
  // A namespaced logger so a module's log lines are attributable + consistent.
39
39
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -53,22 +53,26 @@ test('an unknown age reads as fresh, never as an accusation', () => {
53
53
 
54
54
  test('ageing changes how a row LOOKS, not only where it sits', () => {
55
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.
56
+ // in CSS, or "visibly aging" is just a sort order with extra steps. Since the
57
+ // redesign (task 1003471) the treatment is the age PILL's colour in the row's
58
+ // right column — a glance down the list reads the column — and never a
59
+ // coloured band down the row's edge, which this world does not draw.
57
60
  const css = read('modules', 'hall-ui', 'public', 'collab.css');
58
61
  const L = lib();
59
62
  for (const b of L.BANDS) {
60
- assert.match(css, new RegExp(`\\.cb-item--${b.id}\\b`), `${b.id} has its own styling`);
63
+ assert.match(css, new RegExp(`^\\.cb-age--${b.id}\\s*\\{[^}]*color: var\\(--`, 'm'), `${b.id} has its own colour`);
61
64
  }
62
- assert.match(css, /\.cb-item--stale\s*\{[^}]*border-left-color/, 'the band reads down a list at a glance');
65
+ assert.match(css, /\.cb-age--stale\s*\{[^}]*var\(--danger\)/, 'stale reads down a list at a glance, in the danger ink');
63
66
  // 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');
67
+ assert.match(css, /\.cb-age--stale\s*\{[^}]*font-weight/, 'and the words are emphasised too');
68
+ assert.ok(!/border-left/.test(css), 'no coloured band');
65
69
  });
66
70
 
67
71
  test('the row markup carries the band class and the label', () => {
68
72
  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');
73
+ assert.match(js, /cb-age cb-age--\$\{band\.id\}/, 'the pill\'s class comes from the band');
71
74
  assert.match(js, /escapeHtml\(band\.label\)/, 'the label is rendered, escaped');
75
+ assert.match(js, /L\.ageBand\(req\.created_at, nowMs\)/, 'through the lib, so the boundaries are the tested ones');
72
76
  });
73
77
 
74
78
  test('oldest first WITHIN the page, in addition to the band', () => {
@@ -143,7 +147,9 @@ test('every CSS token the page uses exists in style.css', () => {
143
147
  const css = read('modules', 'hall-ui', 'public', 'collab.css');
144
148
  const style = read('modules', 'hall-ui', 'public', 'style.css');
145
149
  const used = new Set([...css.matchAll(/var\((--[a-z-]+)\)/g)].map((x) => x[1]));
146
- assert.ok(used.size >= 5);
150
+ // Four since the redesign (task 1003471): the sheet holds only the age
151
+ // bands' inks, the row and the panel being the archetype's and the shell's.
152
+ assert.ok(used.size >= 4);
147
153
  for (const t of used) {
148
154
  assert.ok(new RegExp(`^\\s*${t}:`, 'm').test(style), `${t} must be defined (idea 1000821)`);
149
155
  }
@@ -0,0 +1,214 @@
1
+ // tests/hall_collab_world.mjs — the collaboration pages in the chrome world
2
+ // (task 1003471, goal 1000081): collab and the copy desk, redesigned on the
3
+ // hall's row archetype (oversight.css) rather than re-skinned. Static pins over
4
+ // the shipped source: the family sheet is linked and the page sheets spend only
5
+ // the fifteen, every list is the family's row and the card-in-a-card is gone,
6
+ // state is colour and never opacity, the facts the audit found thrown away (who
7
+ // is asking; the duplicate total; the surface count) are rendered, the server
8
+ // names the asker, the dialogs are the family modal, the lists that grow are
9
+ // paged through the kit, and the harness carries what the pages read.
10
+ import test from 'node:test';
11
+ import assert from 'node:assert/strict';
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
17
+ const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
18
+ const FIX = path.join(ROOT, 'scripts', 'hall-preview', 'fixtures');
19
+ const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8').replace(/\r\n/g, '\n');
20
+ const readRoot = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8').replace(/\r\n/g, '\n');
21
+ const PAGES = ['collab', 'copy-desk'];
22
+
23
+ // collab-lib.js is pure, so RUN it rather than grep it.
24
+ function lib() {
25
+ const win = {};
26
+ new Function('window', read('collab-lib.js'))(win);
27
+ return win.OTBCollabLib;
28
+ }
29
+
30
+ test('both pages link the family sheet, before their own', () => {
31
+ for (const p of PAGES) {
32
+ const html = read(`${p}.html`);
33
+ const fam = html.indexOf('/builders/oversight.css');
34
+ const own = html.indexOf(`/builders/${p}.css`);
35
+ assert.ok(fam > 0, `${p}.html links oversight.css`);
36
+ assert.ok(own > fam, `${p}.html links its own sheet after the family's`);
37
+ }
38
+ });
39
+
40
+ test('the page sheets spend only the fifteen, in px, and never opacity', () => {
41
+ for (const p of PAGES) {
42
+ const css = read(`${p}.css`);
43
+ const hexes = css.match(/#[0-9a-f]{3,8}\b/gi) || [];
44
+ assert.deepEqual(hexes, [], `${p}.css carries literal colours: ${hexes.join(', ')}`);
45
+ assert.ok(!/\d(\.\d+)?rem\b/.test(css), `${p}.css sizes in px plus tokens, not rem`);
46
+ assert.ok(!/opacity\s*:/.test(css), `${p}.css carries state as colour, never opacity`);
47
+ assert.ok(!/border-left/.test(css), `${p}.css paints no coloured band`);
48
+ }
49
+ // The one literal the copy desk keeps: the native dialog's scrim.
50
+ const cd = read('copy-desk.css');
51
+ const literals = cd.match(/rgba?\([^)]*\)/g) || [];
52
+ assert.deepEqual(literals, ['rgba(0, 0, 0, 0.55)'], 'black with alpha as a scrim, and nothing else');
53
+ assert.match(cd, /\.cd-dialog::backdrop \{ background: rgba\(0, 0, 0, 0\.55\); \}/, 'and it is spent on ::backdrop');
54
+ });
55
+
56
+ test('every list is the family\'s row, and the card-in-a-card is gone', () => {
57
+ for (const p of PAGES) assert.match(read(`${p}.js`), /class="ov-row/, `${p}.js renders .ov-row`);
58
+ // A class name, whole: `cd-stat` must not match the `cd-status` filter id.
59
+ const gone = (src, cls) => !new RegExp(`${cls}(?![a-z])`).test(src);
60
+ for (const old of ['cb-item', 'cb-list', 'cb-ctx']) {
61
+ assert.ok(gone(read('collab.js'), old) && gone(read('collab.css'), `\\.${old}`), `collab no longer emits ${old}`);
62
+ }
63
+ for (const old of ['cd-card', 'cd-stat', 'cd-tag', 'cd-filters__select', 'cd-browse', 'cd-empty']) {
64
+ assert.ok(gone(read('copy-desk.js'), old) && gone(read('copy-desk.css'), `\\.${old}`), `copy desk no longer emits ${old}`);
65
+ }
66
+ for (const p of PAGES) assert.match(read(`${p}.html`), /class="empty-state"/, `${p}.html empties with the kit's own state`);
67
+ });
68
+
69
+ // ---- collab: the facts the audit found thrown away ---------------------------
70
+ test('collab renders WHO IS ASKING, as a door to their profile', () => {
71
+ const L = lib();
72
+ assert.equal(L.askerOf({ requested_by: 92, requested_by_name: 'Preview Gamma', requested_by_login: 'preview-gamma' }), 'Preview Gamma');
73
+ assert.equal(L.askerOf({ requested_by: 92, requested_by_login: 'preview-gamma' }), 'preview-gamma', 'the login when there is no display name');
74
+ assert.equal(L.askerOf({ requested_by: 92 }), 'builder 92', 'an older server\'s bare id is still a name, never nothing');
75
+ const js = read('collab.js');
76
+ assert.match(js, /L\.askerOf\(req\)/, 'the row asks the lib who is asking');
77
+ assert.match(js, /\/builders\/profile\?id=\$\{escapeHtml\(String\(req\.requested_by\)\)\}/, 'and links the hall\'s per-builder profile');
78
+ assert.match(js, /asked by \$\{who\}/, 'as a fact on the row');
79
+ });
80
+
81
+ test('the server names the people on a row through a join, and the writes stay bare', () => {
82
+ const src = readRoot('modules', 'lifecycle', 'help-requests.js');
83
+ assert.match(src, /requested_by_login, a\.display_name AS requested_by_name/, 'the asker is joined');
84
+ assert.match(src, /needs_builder_login, n\.display_name AS needs_builder_name/, 'and the by-name addressee');
85
+ assert.match(src, /LEFT JOIN builders a ON a\.id = h\.requested_by/, 'a LEFT join, so a row never vanishes with its builder');
86
+ for (const fn of ['listOpenForBuilder', 'listMine']) {
87
+ const body = src.slice(src.indexOf(`async function ${fn}`));
88
+ assert.match(body.slice(0, 700), /SELECT \$\{LIST_COLS\} \$\{LIST_FROM\}/, `${fn} reads the joined columns`);
89
+ }
90
+ const upd = src.slice(src.indexOf('async function setHelpRequestStatus'));
91
+ assert.match(upd.slice(0, 600), /RETURNING \$\{COLS\}/, 'the UPDATE cannot reach a joined alias, so it returns the bare columns');
92
+ });
93
+
94
+ test('the first line is decided: the ask\'s first sentence is the title, the rest the sub, no word lost', () => {
95
+ const L = lib();
96
+ const two = L.titleOf('The record exits to the board. I cannot tell which link does it.');
97
+ assert.deepEqual(two, { title: 'The record exits to the board.', rest: 'I cannot tell which link does it.' });
98
+ const one = L.titleOf('Is there a safe order of operations?');
99
+ assert.deepEqual(one, { title: 'Is there a safe order of operations?', rest: '' }, 'one sentence is the whole title');
100
+ const long = 'A'.repeat(150) + '. Then more.';
101
+ assert.equal(L.titleOf(long).title, long, 'a first sentence too long to be a line keeps the whole ask together');
102
+ assert.equal(L.titleOf('docs/x.md is stale and y.js too').title, 'docs/x.md is stale and y.js too', 'a dot inside a path is not a sentence break');
103
+ const t = L.titleOf(' padded. and trailing ');
104
+ assert.equal(`${t.title} ${t.rest}`, 'padded. and trailing', 'nothing is clipped');
105
+ const js = read('collab.js');
106
+ assert.match(js, /L\.titleOf\(req\.what_is_stuck\)/);
107
+ assert.match(js, /class="ov-row__title">\$\{escapeHtml\(t\.title\)\}/);
108
+ assert.match(js, /class="ov-row__sub">\$\{escapeHtml\(t\.rest\)\}/);
109
+ });
110
+
111
+ test('"asked of you" reads in runs — by name first, then each craft', () => {
112
+ const L = lib();
113
+ assert.equal(L.groupOf({ needs_builder_id: 95 }).id, 'you');
114
+ assert.equal(L.groupOf({ needs_craft: 'artist' }).label, 'asked of artists');
115
+ assert.match(L.addressedTo({ needs_builder_id: 7, needs_builder_name: 'Preview Alpha' }, 9), /Preview Alpha by name/, 'a named addressee is named');
116
+ const js = read('collab.js');
117
+ assert.match(js, /ov-group__head/, 'each run has the family\'s group head');
118
+ assert.match(js, /Number\(b\.id === 'you'\) - Number\(a\.id === 'you'\)/, 'and the personal run sorts first');
119
+ });
120
+
121
+ test('the age is a state pill in the row\'s right column, and every band has its own colour', () => {
122
+ const css = read('collab.css');
123
+ const L = lib();
124
+ for (const b of L.BANDS) assert.match(css, new RegExp(`^\\.cb-age--${b.id}\\s*\\{[^}]*color: var\\(--`, 'm'), `${b.id} has its own colour rule`);
125
+ assert.match(css, /\.cb-age--stale\s*\{[^}]*var\(--danger\)/, 'stale is the danger ink');
126
+ assert.match(css, /\.cb-age--aging\s*\{[^}]*var\(--warn\)/, 'aging is the warn ink');
127
+ const js = read('collab.js');
128
+ assert.match(js, /class="fact-pill cb-age cb-age--\$\{band\.id\}"/, 'the pill is the shell\'s fact-pill');
129
+ assert.match(js, /<div class="ov-row__right">\$\{ageChip\(req, nowMs\)\}/, 'in the right column, so a glance down the list reads the ages');
130
+ });
131
+
132
+ // ---- the copy desk: quieter, and no fact lost ----------------------------------
133
+ test('the copy desk\'s counts are the shell\'s glass strip, carrying two facts the boxes dropped', () => {
134
+ assert.match(read('copy-desk.html'), /class="stat-row ov-summary cd-summary" id="cd-summary-scroll"/);
135
+ const js = read('copy-desk.js');
136
+ assert.match(js, /stat-tile__num/, 'the figure is the shell\'s tile');
137
+ assert.match(js, /of \$\{int\(c\.duplicates_total\)\} said more than once/, 'the duplicate total was never shown before');
138
+ assert.match(js, /across \$\{int\(surfaces\)\} surface/, 'nor the surface count');
139
+ assert.ok(!/cd-stat__n/.test(js), 'the bordered box is gone');
140
+ });
141
+
142
+ test('state is colour from the verification vocabulary, never opacity', () => {
143
+ const js = read('copy-desk.js');
144
+ assert.match(js, /pill\('fact-pill--open', 'already flagged'\)/);
145
+ assert.match(js, /pill\('fact-pill--ok', 'looked at'\)/);
146
+ assert.match(js, /pill\('', 'untouched'\)/, 'the untouched row is the plain pill: colour marks the exception in a list that is mostly work');
147
+ assert.match(js, /pill\('fact-pill--warn', 'target gone'/);
148
+ assert.match(js, /f\.status === 'resolved' \? 'fact-pill--ok' : 'fact-pill--quiet'/);
149
+ });
150
+
151
+ test('the filters and the search are the kit\'s bar; the duplicates are paged through the kit', () => {
152
+ const js = read('copy-desk.js');
153
+ assert.equal((js.match(/kit\.makeFilterBar\(\{/g) || []).length, 2, 'two bars: the queue\'s filters and the search');
154
+ for (const id of ['cd-status', 'cd-surface', 'cd-search']) assert.match(js, new RegExp(`id: '${id}'`), `${id} is a kit control`);
155
+ assert.match(js, /kit\.makePager\('cd-dupes'/);
156
+ assert.match(js, /kit\.paginate\(ordered, dupePager\)/);
157
+ assert.match(js, /kit\.wirePager\(dupePager, rerender\)/);
158
+ assert.match(read('copy-desk.html'), /src="\/builders\/hall-kit\.js/, 'the page loads the kit');
159
+ });
160
+
161
+ test('the two dialogs are the family modal on a native <dialog>, with the number input in a field', () => {
162
+ const html = read('copy-desk.html');
163
+ assert.equal((html.match(/<dialog id="cd-(close-)?dialog" class="ov-modal cd-dialog">/g) || []).length, 2);
164
+ assert.match(html, /<div class="ov-field ov-field--wide">\s*<label class="ov-field__l" for="cd-close-task">[^<]*<\/label>\s*<input type="number" id="cd-close-task"/, 'the number input sits in the family field, which restates the 30px pill');
165
+ assert.ok(!/<label[^>]*>[^<]*<span class="cd-dialog__hint"/.test(html), 'no hint lives inside a label (the label voice would uppercase it)');
166
+ assert.equal((html.match(/class="ov-form__why"/g) || []).length, 3, 'the three hints are the form\'s own explainer line');
167
+ assert.match(html, /class="btn-accent" id="cd-submit"/, 'the accent door files the flag');
168
+ });
169
+
170
+ test('the copy desk renders MORE facts per row than any other page, and lost none', () => {
171
+ const js = read('copy-desk.js');
172
+ for (const fact of ['flagged_by_name', 'flagged_by_login', 'flagged_at', 'resolution_note', 'resolved_task_id',
173
+ 'text_at_flag', 'file_at_flag', 'line_at_flag', 'f.current.file', 'f.current.line', 'f.moved', 'target_state',
174
+ 'd.at', 'd.occurrences', 'd.surface', 'e.origin', 'e.file', 'truncated', 'blind_spots', 'freshness_check', 'r.at']) {
175
+ assert.ok(js.includes(fact), `${fact} is still rendered`);
176
+ }
177
+ assert.match(js, /said, when flagged:/, 'the snapshot is still labelled as the snapshot');
178
+ assert.match(js, /whole surface/, 'a surface-scope flag still says so');
179
+ });
180
+
181
+ test('the closing floor is the pack\'s rank label, not a Greek name in the markup', () => {
182
+ assert.ok(!/Metic\+/.test(read('copy-desk.html')), 'no "Metic+" in the lede');
183
+ assert.match(read('copy-desk.html'), /<span id="cd-close-rank">/);
184
+ assert.match(read('copy-desk.js'), /rankLabel\('metic'\)/);
185
+ });
186
+
187
+ // ---- the harness carries what the pages read ------------------------------------
188
+ test('the harness fixtures exist and carry the shapes the pages read', () => {
189
+ const fx = (n) => JSON.parse(fs.readFileSync(path.join(FIX, `${n}.json`), 'utf8'));
190
+ const forMe = fx('help-requests__for-me');
191
+ assert.ok(Array.isArray(forMe.help_requests) && forMe.help_requests.length >= 4, 'four asks, one per band');
192
+ assert.ok(forMe.help_requests.every((r) => r.requested_by_name && r.what_is_stuck && r.created_at), 'each names its asker');
193
+ assert.ok(forMe.help_requests.some((r) => r.needs_builder_id != null), 'one is by name');
194
+ assert.deepEqual(forMe.crafts, ['builder']);
195
+ const mine = fx('help-requests__mine');
196
+ assert.ok(mine.help_requests.length >= 2 && mine.help_requests.every((r) => String(r.requested_by) === '95'), 'the viewer\'s own (me.json is builder 95)');
197
+ const q = fx('copy-desk__queue');
198
+ assert.ok(q.flags.some((f) => f.target_state === 'gone') && q.flags.some((f) => f.moved) && q.flags.some((f) => f.scope === 'surface'), 'a gone target, a moved one, a whole-surface one');
199
+ assert.ok(q.flags.some((f) => f.status === 'resolved') && q.flags.some((f) => f.status === 'dismissed'), 'and both closed outcomes');
200
+ assert.ok(q.duplicates.length >= 8 && q.duplicates.every((d) => Array.isArray(d.at) && d.at.length === d.occurrences), 'real duplicates with their places');
201
+ assert.ok(q.registry.at && q.registry.freshness_check && q.registry.counts && q.registry.surfaces, 'the registry block the note reads');
202
+ const s = fx('copy-desk__strings');
203
+ assert.ok(Array.isArray(s.entries) && s.entries.length > 0 && typeof s.matched === 'number' && 'truncated' in s && 'limit' in s);
204
+ });
205
+
206
+ test('the states files walk the new states', () => {
207
+ const cd = JSON.parse(read('copy-desk.states.json'));
208
+ assert.deepEqual(Object.keys(cd.states), ['queue', 'out', 'search', 'composer', 'closer']);
209
+ assert.deepEqual(cd.states.search.actions[1], ['fill', '#cd-search', 'save']);
210
+ assert.deepEqual(cd.states.composer.actions[1], ['clickIfVisible', '.cd-flag-btn'], 'clickIfVisible: against the kit\'s stub the button does not exist');
211
+ assert.deepEqual(cd.states.closer.actions[1], ['clickIfVisible', '.cd-close-btn']);
212
+ const cb = JSON.parse(read('collab.states.json'));
213
+ assert.deepEqual(cb.states.board.expect.visible, ['#cb-for-me']);
214
+ });