@bongos/core 1.19.462 → 1.19.464

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.
Files changed (42) hide show
  1. package/.bongos-core.json +92 -42
  2. package/.claude/skills/design/SKILL.md +5 -1
  3. package/docs/adr/0222-the-image-family-under-the-hero-only-rule.md +36 -0
  4. package/docs/adr/README.md +1 -0
  5. package/docs/copy-inventory.md +122 -125
  6. package/docs/copy-registry.json +306 -322
  7. package/docs/file-map.md +2 -2
  8. package/docs/module-api-changelog.md +4 -0
  9. package/modules/hall-ui/public/goals-lib.js +10 -6
  10. package/modules/hall-ui/public/goals-page.js +27 -12
  11. package/modules/hall-ui/public/goals-render.js +83 -52
  12. package/modules/hall-ui/public/goals.css +129 -120
  13. package/modules/hall-ui/public/goals.html +29 -11
  14. package/modules/hall-ui/public/goals.states.json +71 -3
  15. package/modules/hall-ui/public/people.css +27 -0
  16. package/modules/hall-ui/public/people.html +15 -7
  17. package/modules/hall-ui/public/people.js +111 -22
  18. package/modules/hall-ui/public/people.states.json +31 -1
  19. package/modules/hall-ui/public/scouting.html +30 -17
  20. package/modules/hall-ui/public/scouting.states.json +7 -2
  21. package/modules/hall-ui/public/sessions.css +18 -15
  22. package/modules/hall-ui/public/sessions.html +27 -22
  23. package/modules/hall-ui/public/sessions.js +32 -13
  24. package/modules/hall-ui/public/sessions.states.json +3 -2
  25. package/modules/ui-design/module.json +1 -1
  26. package/modules/ui-design/skills/README.md +6 -2
  27. package/modules/ui-design/skills/brandkit/SKILL.md +62 -0
  28. package/modules/ui-design/skills/image-to-code/SKILL.md +66 -0
  29. package/modules/ui-design/skills/imagegen-frontend-mobile/SKILL.md +62 -0
  30. package/modules/ui-design/skills/imagegen-frontend-web/SKILL.md +83 -0
  31. package/modules/ui-design/styles/grove/DESIGN.md +1 -1
  32. package/modules/ui-design/styles/grove/assets/grove-pod-cut.webp +0 -0
  33. package/modules/ui-design/styles/grove/assets/grove-pod-cut.webp.json +12 -0
  34. package/modules/ui-design/styles/grove/assets/grove-pod.jpg +0 -0
  35. package/modules/ui-design/styles/grove/assets/grove-pod.jpg.json +12 -0
  36. package/modules/ui-design/styles/grove/mock.html +15 -1
  37. package/package-lock.json +2 -2
  38. package/package.json +1 -1
  39. package/src/module-api.js +1 -1
  40. package/tests/hall_roster_world.mjs +287 -0
  41. package/tests/ui_design_skills.mjs +52 -0
  42. package/modules/hall-ui/public/scouting.js +0 -73
@@ -0,0 +1,287 @@
1
+ // tests/hall_roster_world.mjs — the roster and ledger pages in the chrome world
2
+ // (task 1003470, goal 1000081): goals, sessions, people and scouting,
3
+ // REDESIGNED rather than re-skinned. Static pins over the shipped source plus
4
+ // two runs of the pure goals lib: the goals list is one panel with three lenses
5
+ // in the open and the rest behind the kit's disclosure, the readiness is the
6
+ // family's figure band in the same dot language as the roll line, the view
7
+ // toggle is a view and not a filter, the accent lands on the one live door; the
8
+ // two directory pages are ONE page (the legacy door renders the same body from
9
+ // the same script) and the directory shows the avatar, the name and the login
10
+ // the API returns; the session ledger drops the nowrap + wrapper pair that broke
11
+ // its sticky header, makes every id a door, and names its floor from the pack.
12
+ import test from 'node:test';
13
+ import assert from 'node:assert/strict';
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import vm from 'node:vm';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
20
+ const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
21
+ const FIX = path.join(ROOT, 'scripts', 'hall-preview', 'fixtures');
22
+ const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8').replace(/\r\n/g, '\n');
23
+ const readJson = (f) => JSON.parse(fs.readFileSync(path.join(FIX, f), 'utf8'));
24
+ // Negative pins read the code, not its comments (a comment that says what left
25
+ // would otherwise fail the pin that says it left).
26
+ const decomment = (src) => src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
27
+ const STYLE = read('style.css');
28
+ const OVERSIGHT = read('oversight.css');
29
+ // A token is defined in the shell, the family sheet, or the page sheet itself
30
+ // (goals.css declares --ring, the ring column's width, on its own list).
31
+ const tokensDefined = (css, where) => {
32
+ for (const t of new Set([...css.matchAll(/var\((--[a-z0-9-]+)\)/g)].map((m) => m[1]))) {
33
+ const declared = (src) => new RegExp(`(^\\s*|[{;]\\s*)${t}:`, 'm').test(src);
34
+ assert.ok(declared(STYLE) || declared(OVERSIGHT) || declared(css),
35
+ `${where} spends ${t}, which neither style.css, oversight.css nor the sheet itself defines`);
36
+ }
37
+ };
38
+ const noAlias = (rawCss, where) => {
39
+ const css = decomment(rawCss);
40
+ const alias = css.match(/--(surface|surface-2|border|border-strong|accent-weak|accent-text)\b/g) || [];
41
+ assert.deepEqual(alias, [], `${where} spends a compat alias: ${alias.join(', ')}`);
42
+ const hexes = css.match(/#[0-9a-f]{3,8}\b/gi) || [];
43
+ assert.deepEqual(hexes, [], `${where} carries literal colours: ${hexes.join(', ')}`);
44
+ };
45
+ const block = (css, sel) => {
46
+ const i = css.indexOf(`\n${sel} {`);
47
+ assert.ok(i >= 0, `${sel} has a rule`);
48
+ return css.slice(i, css.indexOf('}', i));
49
+ };
50
+
51
+ // goals-lib.js is pure: RUN it under the two globals it reads.
52
+ function goalsLib() {
53
+ const sandbox = {
54
+ window: {
55
+ OTB: { escapeHtml: (s) => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])) },
56
+ OTBTaskLib: { msOf: () => 0, taskActivityMs: () => 0 },
57
+ },
58
+ };
59
+ vm.createContext(sandbox);
60
+ vm.runInContext(read('goals-lib.js'), sandbox, { filename: 'goals-lib.js' });
61
+ return sandbox.window.OTBGoalsLib;
62
+ }
63
+
64
+ // =========================================================================
65
+ // GOALS — the archetype page
66
+ // =========================================================================
67
+
68
+ test('goals: the list is ONE panel — the shell\'s .scroll around the filters, the count line and the groups', () => {
69
+ const html = read('goals.html');
70
+ const list = html.slice(html.indexOf('id="goals-list-view"'), html.indexOf('id="goal-detail-view"'));
71
+ assert.match(list, /<div class="scroll goals-panel">/, 'the panel is the shell\'s .scroll');
72
+ for (const id of ['goals-filters', 'goals-count', 'goals-view', 'goal-grid', 'goals-pager-mount']) {
73
+ assert.ok(list.includes(`id="${id}"`), `#${id} sits inside the list view`);
74
+ }
75
+ const panelStart = list.indexOf('class="scroll goals-panel"');
76
+ assert.ok(list.indexOf('id="goals-filters"') > panelStart, 'the filter bar is INSIDE the panel, not on the ground above it');
77
+ const fam = html.indexOf('/builders/oversight.css');
78
+ assert.ok(fam > 0 && html.indexOf('/builders/goals.css') > fam, 'goals.html links the family sheet before its own');
79
+ const css = read('goals.css');
80
+ assert.doesNotMatch(block(css, '.gr-list'), /background:\s*var\(--bg-card\)|border-radius/, '.gr-list is no longer a second panel per group');
81
+ });
82
+
83
+ test('goals: three lenses in the open, five behind the kit\'s disclosure, and no View select', () => {
84
+ const src = read('goals-page.js');
85
+ const spec = src.slice(src.indexOf("mount: $('#goals-filters')"), src.indexOf('onChange: onListFilterChange'));
86
+ const ids = (part) => [...part.matchAll(/id: '(f-goal-[a-z-]+)'/g)].map((m) => m[1]);
87
+ const primary = spec.slice(spec.indexOf('primary: ['), spec.indexOf('overflow: ['));
88
+ const overflow = spec.slice(spec.indexOf('overflow: ['));
89
+ assert.deepEqual(ids(primary), ['f-goal-version', 'f-goal-status', 'f-goal-order'], 'the open bar is version, state, order');
90
+ assert.deepEqual(ids(overflow), ['f-goal-mine', 'f-goal-owner', 'f-goal-category', 'f-goal-role', 'f-goal-stale'],
91
+ 'the five rarely-turned lenses sit behind Filters — where the kit counts and chips them');
92
+ assert.ok(!spec.includes("id: 'f-goal-view'"), 'rows|cards is not a filter-bar control');
93
+ });
94
+
95
+ test('goals: rows|cards is a segmented view toggle that still writes #f-goal-view and the saved pref', () => {
96
+ const html = read('goals.html');
97
+ assert.match(html, /<div class="viewtoggle" role="group" aria-label="View" id="goals-view">/);
98
+ assert.match(html, /<input type="hidden" id="f-goal-view" value="list">/, 'the hidden input is what renderList forks on');
99
+ assert.match(html, /data-view="list" aria-pressed="true"/);
100
+ assert.match(html, /data-view="cards" aria-pressed="false"/);
101
+ const src = read('goals-page.js');
102
+ assert.match(src, /function setGoalView\(view, opts\)/);
103
+ assert.match(src, /b\.setAttribute\('aria-pressed', String\(b\.dataset\.view === view\)\)/, 'the buttons carry the pressed state');
104
+ assert.match(src, /if \(savedGoalView === 'cards'\) setGoalView\('cards', \{ fire: false \}\)/, 'the saved pref restores the toggle without a render');
105
+ const css = read('goals.css');
106
+ const pressed = block(css, '.viewtoggle__btn[aria-pressed="true"]');
107
+ assert.match(pressed, /var\(--accent-tint\)/, 'the pressed side takes the current-thing tint');
108
+ assert.doesNotMatch(pressed, /var\(--accent\)/, 'never the accent fill: a view is not a live door');
109
+ });
110
+
111
+ test('goals: the readiness is the family figure band, in the roll line\'s own dot language', () => {
112
+ const src = read('goals-render.js');
113
+ const body = src.slice(src.indexOf('function renderList'), src.indexOf('\n function ', src.indexOf('function renderList') + 1));
114
+ assert.match(body, /<div class="ov-figs gr-figs">/, 'the band is .ov-figs');
115
+ for (const label of ['Ready to claim', 'In progress', 'Blocked', 'Criteria met']) {
116
+ assert.ok(body.includes(`fig('${label}'`), `the band carries "${label}"`);
117
+ }
118
+ assert.match(body, /gr-mark gr-mark--\$\{mark\}/, 'the label\'s dot is the shared .gr-mark');
119
+ assert.match(body, /<div class="ov-group__head"><span class="ov-group__name">/, 'the groups take the family head');
120
+ for (const gone of ['gr-summary', 'gr-sec', 'gr-sm']) assert.ok(!src.includes(gone), `${gone} is gone`);
121
+ const lib = goalsLib();
122
+ const roll = lib.goalRollLine({ ready: 1, active: 0, blocked: 2, backlog: 0 });
123
+ assert.match(roll, /gr-mark gr-mark--ready/);
124
+ assert.match(roll, /gr-mark gr-mark--blocked/);
125
+ assert.doesNotMatch(roll, /style=/, 'the dot colours are classes, not inline styles');
126
+ const css = read('goals.css');
127
+ assert.match(block(css, '.gr-mark--blocked'), /var\(--warn\)/, 'blocked is the warn colour, the pill\'s own');
128
+ });
129
+
130
+ test('goals: the row\'s right column is one action or one state, and the accent lands on the live door only', () => {
131
+ const src = read('goals-render.js');
132
+ const facts = src.slice(src.indexOf('function goalRenderFacts'), src.indexOf('function goalRowHtml'));
133
+ assert.match(facts, /class="btn-accent gr-claim" href="#\/task\//, '"claim next" is the shell\'s accent door');
134
+ assert.match(facts, /class="btn-ghost gr-drill"/, 'the blocked door is a ghost pill');
135
+ assert.match(facts, /gr-state gr-state--ok">\$\{checkSvg\('gr-state__mark'\)\}achieved/, 'achieved is a drawn check, not a glyph');
136
+ assert.doesNotMatch(facts, /[✓✔]/, 'no typed check mark');
137
+ assert.match(facts, /\+ chips\.join\(''\)\n\s*\+ catHtml\n/, 'the category chip rides in the facts line with the others');
138
+ assert.doesNotMatch(src, /class="gr-meta"/, 'the meta run under the title is gone');
139
+ const lib = goalsLib();
140
+ assert.match(lib.ring(1, 3, false), /stroke="var\(--ink\)"/, 'an open goal\'s arc is ink');
141
+ assert.match(lib.ring(3, 3, true), /stroke="var\(--ok\)"/, 'an achieved goal\'s arc is the ok colour');
142
+ assert.doesNotMatch(read('goals-lib.js'), /var\(--accent\)/, 'the lib spends no accent');
143
+ });
144
+
145
+ test('goals: the card is the row folded — no box, one fact source, the subtitle as its tooltip', () => {
146
+ const src = read('goals-render.js');
147
+ const card = src.slice(src.indexOf('function goalCardHtml'), src.indexOf('function renderList'));
148
+ assert.match(card, /\$\{factsHtml\}/, 'the card renders the same facts line');
149
+ assert.match(card, /\$\{idHtml\}/, 'with the id inside the title');
150
+ assert.match(card, /class="goal-card__foot">\$\{right\}/, 'and the same right-column state as its foot');
151
+ const css = read('goals.css');
152
+ const cardRule = block(css, '.goal-card');
153
+ assert.doesNotMatch(cardRule, /border(-radius)?:/, 'a card inside the panel draws no box');
154
+ assert.match(block(css, '.goal-grid'), /gap: 1px 0; background: var\(--rule-soft\)/, 'the hairline is painted BETWEEN rows by the grid');
155
+ assert.ok(!/goal-card--skel|goal-card--blocked/.test(css), 'the old card skeleton and the dashed blocked card are gone');
156
+ assert.match(read('goals-page.js'), /gr-row gr-row--skel/, 'the boot skeleton is row-shaped');
157
+ });
158
+
159
+ test('goals.css and people.css spend only the fifteen, and every token they name exists', () => {
160
+ for (const f of ['goals.css', 'people.css']) {
161
+ const css = read(f);
162
+ noAlias(css, f);
163
+ tokensDefined(css, f);
164
+ }
165
+ });
166
+
167
+ // =========================================================================
168
+ // PEOPLE + SCOUTING — one directory, two doors
169
+ // =========================================================================
170
+
171
+ test('scouting is the same page as people: one body, one script, no script of its own', () => {
172
+ const people = read('people.html');
173
+ const scouting = read('scouting.html');
174
+ const bodyOf = (html) => html.slice(html.indexOf('<body'));
175
+ assert.equal(bodyOf(scouting), bodyOf(people), 'the two bodies are byte-identical');
176
+ assert.ok(!fs.existsSync(path.join(PUB, 'scouting.js')), 'scouting.js is gone — the third rendering of one endpoint');
177
+ assert.doesNotMatch(scouting, /<style[^>]*>/, 'the legacy door carries no inline style');
178
+ for (const html of [people, scouting]) {
179
+ assert.match(html, /\/builders\/people\.js/);
180
+ assert.match(html, /\/builders\/people\.css/);
181
+ assert.match(html, /\/builders\/dom-utils\.js/, 'the toolkit the rank label and the number formats come from');
182
+ assert.match(html, /<h1 class="page-head__h">People<\/h1>/);
183
+ }
184
+ });
185
+
186
+ test('the directory row shows the avatar, the name and the login the API returns, and the name is a door', () => {
187
+ const src = read('people.js');
188
+ assert.match(src, /b\.avatar_url\s*\?\s*'<img class="dir-avatar" src="' \+ esc\(b\.avatar_url\)/, 'the avatar the pages dropped');
189
+ assert.match(src, /dir-avatar dir-avatar--initial/, 'an initial when there is none');
190
+ assert.match(src, /return b\.display_name \|\| b\.github_login \|\| \('#' \+ b\.github_id\);/, 'the display name reads first');
191
+ assert.match(src, /class="lb-name dir-name" href="https:\/\/github\.com\/' \+ encodeURIComponent\(b\.github_login\)/,
192
+ 'the shell\'s .lb-name, to the one page every listed builder has');
193
+ assert.match(src, /'<span class="dir-login">@' \+ esc\(b\.github_login\)/, 'the login under the name says where the door goes');
194
+ assert.match(src, /<span class="fact-chip">' \+ esc\(String\(d\)\)/, 'crafts are chips');
195
+ assert.doesNotMatch(src, /class="num"/, 'the shell\'s ledger__num, not the bare .num scouting.js had corrected');
196
+ assert.match(src, /class="ledger__num"/);
197
+ });
198
+
199
+ test('the directory has a search, a sort and a craft lens on the kit bar, and pages through the kit', () => {
200
+ const src = read('people.js');
201
+ assert.match(src, /kit\.makeFilterBar\(\{/);
202
+ assert.match(src, /search: \{ id: 'people-q'/);
203
+ assert.match(src, /\{ id: 'people-sort', label: 'Sort'/);
204
+ assert.match(src, /\{ id: 'people-craft', label: 'Craft'/);
205
+ assert.match(src, /kit\.makePager\('people'/);
206
+ assert.match(src, /kit\.makeLedger\(\{/);
207
+ assert.doesNotMatch(src, /createElement\('table'\)/, 'no hand-built table');
208
+ });
209
+
210
+ test('the directory names its floor from the pack, and never says Archon-only', () => {
211
+ const src = read('people.js');
212
+ assert.match(src, /rankLabel\('metic'\)/);
213
+ assert.doesNotMatch(decomment(src), /Archon-only/);
214
+ assert.doesNotMatch(read('people.html'), /Archon/);
215
+ });
216
+
217
+ // =========================================================================
218
+ // SESSIONS
219
+ // =========================================================================
220
+
221
+ test('sessions: the nowrap + wrapper pair is gone, and with it the sticky-header overlay', () => {
222
+ const css = decomment(read('sessions.css'));
223
+ assert.doesNotMatch(css, /white-space:\s*nowrap/, 'no blanket nowrap on every cell');
224
+ assert.doesNotMatch(css, /ledger-table-wrap/, 'no wrapper — the shell\'s card strip handles a phone');
225
+ assert.doesNotMatch(read('sessions.html'), /ledger-table-wrap/);
226
+ assert.doesNotMatch(css, /var\(--info\)|ledger-sid/, 'the session id is no longer painted in the info colour');
227
+ noAlias(css, 'sessions.css');
228
+ tokensDefined(css, 'sessions.css');
229
+ });
230
+
231
+ test('sessions: every id on a row is a door, the earned figure is shown, the totals ride the top strip', () => {
232
+ const src = read('sessions.js');
233
+ assert.match(src, /<a class="fact-chip" href="\/builders\/task\/\$\{encodeURIComponent\(String\(t\)\)\}">task \$\{escapeHtml\(String\(t\)\)\}<\/a>/,
234
+ 'a task id is a chip to the task page');
235
+ assert.match(src, /<a class="lb-name" href="\/builders\/profile\?id=\$\{encodeURIComponent\(String\(s\.builder_id\)\)\}">/, 'the builder is a door to their profile');
236
+ assert.match(src, /<span class="ov-mono" title="\$\{escapeHtml\(s\.session_id\)\}">/, 'the session id is a mono fact with the full id on hover');
237
+ assert.match(src, /fmtInt\(s\.drachmae_earned\)/, 'what the session earned is on the row');
238
+ assert.doesNotMatch(src, /`#\$\{escapeHtml\(t\)\}`/, 'no bare hash-prefixed task text');
239
+ const html = read('sessions.html');
240
+ assert.match(html, /<section class="ov-summary" id="stats-scroll"/, 'the totals are the top-of-page strip');
241
+ assert.match(html, /<div class="stat-row" id="stats-body"><\/div>/);
242
+ const ths = (html.match(/<th[ >]/g) || []).length;
243
+ assert.equal(ths, 9, 'nine columns: session, builder, tasks, earned, turns, tokens, tool errors, duration, recorded');
244
+ assert.match(src, /colspan="9"/, 'the empty row spans them all');
245
+ const fam = html.indexOf('/builders/oversight.css');
246
+ assert.ok(fam > 0 && html.indexOf('/builders/sessions.css') > fam, 'sessions.html links the family sheet before its own');
247
+ });
248
+
249
+ test('sessions: the floor is written from the pack\'s rank label, never a hardcoded rank', () => {
250
+ const src = read('sessions.js');
251
+ const sealed = src.slice(src.indexOf('function showSealed'), src.indexOf('// ---- paging state'));
252
+ assert.match(sealed, /const floor = rankLabel\('metic'\)/);
253
+ assert.match(sealed, /\$\('#gate-h'\)\.textContent = `\$\{floor\} and up`/);
254
+ assert.doesNotMatch(decomment(src), /Archons only|Metic rank up|Archon-only/);
255
+ assert.doesNotMatch(read('sessions.html'), /Archon/);
256
+ });
257
+
258
+ // =========================================================================
259
+ // The harness carries what the pages read, and the states files ask for the new states
260
+ // =========================================================================
261
+
262
+ test('the harness feeds: rollups for every goal, the category set, avatars on the directory', () => {
263
+ const goals = readJson('goals.json').goals;
264
+ const rollups = readJson('goals__rollups.json').rollups;
265
+ const byGoal = new Set(rollups.map((r) => String(r.goal_id)));
266
+ for (const g of goals) assert.ok(byGoal.has(String(g.id)), `goal ${g.id} has a rollup row`);
267
+ assert.ok(rollups.some((r) => r.next_ready_id), 'at least one row has a claimable next task');
268
+ assert.ok(rollups.some((r) => r.blocked > 0 && r.ready === 0 && r.active === 0), 'at least one goal is blocked-only');
269
+ assert.ok(goals.some((g) => g.subtitle), 'some goals carry a subtitle');
270
+ assert.ok(goals.some((g) => g.category && g.category.slug), 'some goals carry a category');
271
+ const cats = readJson('work-categories.json').categories;
272
+ assert.ok(cats.length >= 2 && cats.every((c) => c.slug && c.label));
273
+ const dir = readJson('scouting.json').builders;
274
+ assert.ok(dir.some((b) => b.avatar_url) && dir.some((b) => !b.avatar_url), 'the directory fixture has builders with and without an avatar');
275
+ });
276
+
277
+ test('the states files exercise the redesign\'s new states', () => {
278
+ const goals = JSON.parse(read('goals.states.json'));
279
+ assert.ok(goals.states.filters && goals.states.cards, 'goals: the Filters disclosure and the cards view are measured');
280
+ assert.deepEqual(goals.states.cards.expect.value, [['#f-goal-view', 'cards']]);
281
+ const people = JSON.parse(read('people.states.json'));
282
+ assert.ok(people.states.search, 'people: a search is measured');
283
+ const scouting = JSON.parse(read('scouting.states.json'));
284
+ assert.deepEqual(scouting.states.board.expect.visible, ['#people-scroll'], 'scouting expects the People panel');
285
+ const sessions = JSON.parse(read('sessions.states.json'));
286
+ assert.deepEqual(sessions.states.ledger.expect.visible, ['#stats-scroll', '#sessions-scroll']);
287
+ });
@@ -148,6 +148,58 @@ test('impeccable carries none of the behaviour that made its origin dangerous: n
148
148
  assert.match(skill, /1003329/, 'the detector is named as its own task, not shipped here');
149
149
  });
150
150
 
151
+ // ── the image family (task 1003328, ADR 0222): plates and hero objects only, never screens ──
152
+ const IMAGE_FAMILY = ['image-to-code', 'imagegen-frontend-web', 'imagegen-frontend-mobile', 'brandkit'];
153
+
154
+ test('the image family of task 1003328 exists as four in-house skills, declared, each naming the sidecar, "text-free" and the art pipeline key in its own rules', () => {
155
+ const pre = read(path.join(SKILLS, 'PREAMBLE.md')).trimEnd();
156
+ for (const name of IMAGE_FAMILY) {
157
+ assert.ok(ownDirs.includes(name), `${name}: skills/${name}/SKILL.md exists`);
158
+ assert.ok(manifest.contributes.skills.includes(name), `${name}: declared in contributes.skills`);
159
+ const text = read(path.join(SKILLS, name, 'SKILL.md'));
160
+ const own = text.slice(text.indexOf(pre) + pre.length);
161
+ assert.match(own, /sidecar/i, `${name}: names the provenance sidecar beside every plate (DESIGN.md's materials contract)`);
162
+ assert.match(own, /text-free/i, `${name}: restates that a plate carries no text`);
163
+ assert.match(own, /art[- ]key|art pipeline/i, `${name}: generation goes through the instance's art pipeline key, never a credential of its own`);
164
+ assert.match(own, /1003328/, `${name}: names the port task`);
165
+ }
166
+ });
167
+
168
+ test('no image-family skill carries the directives the panel flagged: no eagerness, no image-count mandate, no one-image-per-section or per-screen rule, no must-invoke-any-image-tool', () => {
169
+ const pre = read(path.join(SKILLS, 'PREAMBLE.md')).trimEnd();
170
+ for (const name of IMAGE_FAMILY) {
171
+ const text = read(path.join(SKILLS, name, 'SKILL.md'));
172
+ const own = text.slice(text.indexOf(pre) + pre.length);
173
+ assert.doesNotMatch(own, /IMAGE_GENERATION_EAGERNESS|eagerness\s*[:=]/i, `${name}: no eagerness setting`);
174
+ assert.doesNotMatch(own, /do not be lazy|don't be lazy/i, `${name}: no image-count goad`);
175
+ assert.doesNotMatch(own, /\b(exactly|generate|produce|render)\s+one\s+(image|frame|render)\s+per\s+(section|screen)\b/i, `${name}: no one-image-per-section or per-screen rule as a directive (naming the upstream's rule in the Rebuilt-from is fine)`);
176
+ assert.doesNotMatch(own, /\b(must|always)\s+(invoke|call|use)\s+(any|every|an?|the)\s+(available\s+)?image[- ]generat/i, `${name}: no must-invoke-any-image-tool directive`);
177
+ assert.doesNotMatch(own, /scale[s]?\s+(the\s+)?image\s+count\s+with/i, `${name}: no count scaled with sections (naming it as the upstream's is fine; directing it is not)`);
178
+ }
179
+ for (const name of ['imagegen-frontend-mobile', 'brandkit']) {
180
+ const own = read(path.join(SKILLS, name, 'SKILL.md'));
181
+ assert.match(own, /never (a|an) (screen|phone mockup|generated logo)|no phone mockups?/i, `${name}: says what it will not generate`);
182
+ }
183
+ });
184
+
185
+ test('the worked example of the image family is the grove specimen: the pair with a sidecar beside every raster, referenced as tokens on the mock body', () => {
186
+ const grove = path.join(MODULE, 'styles', 'grove');
187
+ for (const f of ['grove-pod.jpg', 'grove-pod-cut.webp']) {
188
+ assert.ok(fs.existsSync(path.join(grove, 'assets', f)), `styles/grove/assets/${f} exists`);
189
+ const side = path.join(grove, 'assets', `${f}.json`);
190
+ assert.ok(fs.existsSync(side), `${f}.json sidecar beside it (named WITH the extension)`);
191
+ const meta = readJson(side);
192
+ for (const k of ['kind', 'model', 'prompt', 'encoded', 'use']) assert.ok(meta[k], `${f}.json carries ${k}`);
193
+ assert.match(meta.prompt, /no text/i, 'the prompt carries the text-free refusal');
194
+ }
195
+ const mock = read(path.join(grove, 'mock.html'));
196
+ assert.match(mock, /--plate-pod:\s*url\(\/assets\/grove-pod\.jpg\)/, 'the dark plate is a token on body, root-absolute');
197
+ assert.match(mock, /--plate-pod-cut:\s*url\(\/assets\/grove-pod-cut\.webp\)/, 'the light cutout is a token on body, root-absolute');
198
+ assert.ok(!/<img\b/i.test(mock), 'the plate is a token, not an <img>');
199
+ const design = read(path.join(grove, 'DESIGN.md'));
200
+ assert.match(design, /grove-pod\.jpg/, 'the look\'s DESIGN.md names its specimen\'s plate instead of claiming it brings none');
201
+ });
202
+
151
203
  test('every in-house SKILL.md opens with the PLATFORM PREAMBLE, byte-identical to PREAMBLE.md, exactly once, right after its frontmatter', () => {
152
204
  const pre = read(path.join(SKILLS, 'PREAMBLE.md')).trimEnd();
153
205
  for (const name of ownDirs) {
@@ -1,73 +0,0 @@
1
- // scouting.js — the internal Archon-only cross-project scouting view (ADR 0141 §4,
2
- // task #1002114). Fetches /api/gds/scouting (Archon-gated server-side) and renders a
3
- // ranked table. A non-Archon gets a 403 and sees the Archon-only notice — the data
4
- // endpoint's rank gate is the real protection ("NOT shown to builders"). All network
5
- // values are rendered via textContent — never innerHTML.
6
- (function () {
7
- 'use strict';
8
- var body = document.getElementById('body');
9
-
10
- function el(tag, cls) { var e = document.createElement(tag); if (cls) e.className = cls; return e; }
11
- function td(text, cls) { var c = el('td', cls); c.textContent = text; return c; }
12
-
13
- function fmtRecency(iso) {
14
- if (!iso) return '—';
15
- var t = Date.parse(iso);
16
- if (isNaN(t)) return '—';
17
- var days = Math.floor((Date.now() - t) / 86400000);
18
- if (days <= 0) return 'today';
19
- if (days === 1) return '1 day ago';
20
- if (days < 30) return days + ' days ago';
21
- return Math.floor(days / 30) + ' mo ago';
22
- }
23
-
24
- function render(builders) {
25
- body.className = '';
26
- body.textContent = '';
27
- if (!builders || builders.length === 0) {
28
- body.className = 'profile__placeholder';
29
- body.textContent = 'No cross-project activity reported yet.';
30
- return;
31
- }
32
- // table.ledger, appended straight into #body: the design system's table
33
- // styling, and #body becomes the :has(> .ledger) phone scroll strip.
34
- var table = el('table', 'ledger');
35
- var thead = el('thead');
36
- var hr = el('tr');
37
- [['Builder', ''], ['Projects', 'ledger__num'], ['Credits', 'ledger__num'], ['Ships', 'ledger__num'], ['Karma', 'ledger__num'], ['Disciplines', ''], ['Last active', '']]
38
- .forEach(function (h) { var th = el('th', h[1]); th.textContent = h[0]; hr.appendChild(th); });
39
- thead.appendChild(hr); table.appendChild(thead);
40
- var tbody = el('tbody');
41
- builders.forEach(function (b) {
42
- var tr = el('tr');
43
- var who = el('td');
44
- var name = el('span', 'who');
45
- name.textContent = b.github_login || b.display_name || ('#' + b.github_id);
46
- who.appendChild(name);
47
- tr.appendChild(who);
48
- tr.appendChild(td(String(b.project_count != null ? b.project_count : '—'), 'ledger__num'));
49
- tr.appendChild(td(String(b.total_credits != null ? b.total_credits : 0), 'ledger__num'));
50
- tr.appendChild(td(String(b.total_tasks_shipped != null ? b.total_tasks_shipped : 0), 'ledger__num'));
51
- tr.appendChild(td(String(b.total_karma != null ? b.total_karma : 0), 'ledger__num'));
52
- tr.appendChild(td(Array.isArray(b.disciplines) ? b.disciplines.join(', ') : '—'));
53
- tr.appendChild(td(fmtRecency(b.last_active)));
54
- tbody.appendChild(tr);
55
- });
56
- table.appendChild(tbody);
57
- body.appendChild(table);
58
- }
59
-
60
- fetch('/api/gds/scouting', { headers: { Accept: 'application/json' } })
61
- .then(function (r) {
62
- if (r.status === 403 || r.status === 401) { throw { archon: true }; }
63
- if (!r.ok) throw new Error('http ' + r.status);
64
- return r.json();
65
- })
66
- .then(function (data) { render(data && data.builders); })
67
- .catch(function (e) {
68
- body.className = 'profile__placeholder';
69
- body.textContent = (e && e.archon)
70
- ? 'This view is Archon-only.'
71
- : 'Couldn’t load the scouting view right now.';
72
- });
73
- }());