@bongos/core 1.19.461 → 1.19.463

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 (51) hide show
  1. package/.bongos-core.json +145 -40
  2. package/.claude/skills/design/SKILL.md +2 -1
  3. package/docs/adr/0221-the-impeccable-method-is-one-skill-with-sub-command-playbooks.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 +4 -3
  27. package/modules/ui-design/skills/impeccable/SKILL.md +143 -0
  28. package/modules/ui-design/skills/impeccable/reference/adapt.md +25 -0
  29. package/modules/ui-design/skills/impeccable/reference/animate.md +27 -0
  30. package/modules/ui-design/skills/impeccable/reference/audit.md +19 -0
  31. package/modules/ui-design/skills/impeccable/reference/bolder.md +17 -0
  32. package/modules/ui-design/skills/impeccable/reference/clarify.md +25 -0
  33. package/modules/ui-design/skills/impeccable/reference/colorize.md +22 -0
  34. package/modules/ui-design/skills/impeccable/reference/critique.md +15 -0
  35. package/modules/ui-design/skills/impeccable/reference/distill.md +26 -0
  36. package/modules/ui-design/skills/impeccable/reference/document.md +25 -0
  37. package/modules/ui-design/skills/impeccable/reference/extract.md +20 -0
  38. package/modules/ui-design/skills/impeccable/reference/harden.md +21 -0
  39. package/modules/ui-design/skills/impeccable/reference/layout.md +20 -0
  40. package/modules/ui-design/skills/impeccable/reference/onboard.md +22 -0
  41. package/modules/ui-design/skills/impeccable/reference/optimize.md +22 -0
  42. package/modules/ui-design/skills/impeccable/reference/polish.md +22 -0
  43. package/modules/ui-design/skills/impeccable/reference/quieter.md +17 -0
  44. package/modules/ui-design/skills/impeccable/reference/shape.md +23 -0
  45. package/modules/ui-design/skills/impeccable/reference/typeset.md +24 -0
  46. package/package-lock.json +2 -2
  47. package/package.json +1 -1
  48. package/src/module-api.js +1 -1
  49. package/tests/hall_roster_world.mjs +287 -0
  50. package/tests/ui_design_skills.mjs +41 -0
  51. package/modules/hall-ui/public/scouting.js +0 -73
@@ -0,0 +1,23 @@
1
+ # `impeccable shape` — the bounded discovery interview
2
+
3
+ **When:** before any new visual work, or when the brief block in the ship notes has holes (no audience, no primary action, no mode). Planning only: **no code, no contract, no persistence** beyond the brief block itself.
4
+
5
+ **Runs in:** any of the four cases. In case 4 (no visual authority) it is also where the session says out loud that the neutral pack is the world.
6
+
7
+ ## The interview
8
+
9
+ One round by default, **two or three questions per round**, each about outcomes and product behaviour rather than implementation (the owner may not be an engineer). Prefer the harness's structured question widget where one exists; otherwise three numbered questions in one message. A second round only when the first left a fork open, and never a third.
10
+
11
+ Pick the questions from what the homes do not already answer. Read `docs/project-context.template.md` and the task body first; ask nothing they settle.
12
+
13
+ - **Who is this for and what do they come to do?** The visitor mode falls out of the answer: persuade, operate, read, or experience. Pick it from the surface, not the product.
14
+ - **What is the one action, and what proof makes them take it?** A ledger's proof is the data itself; a landing's is a plate and a sentence; a settings page's is that nothing is hidden.
15
+ - **What must not change?** The named rules of the world bind by default; the question is which existing content, behaviour or identity is out of scope for this pass.
16
+ - **Refine or redesign?** Say what each means in one line (refinement keeps the look; redesign keeps the truth and replaces the look) and make them pick. No blend.
17
+ - **Mockup first or straight to code?** Asked once, as its own question, and recorded in the brief; a surface with a settled world usually goes straight to code and proves itself in the kit.
18
+
19
+ Where the product-truth file is still the scaffold, the round covers its sections too (platform, users, purpose, positioning, constraints, brand commitments, accessibility), **at least one real answer** before anything is treated as known; an inferred fact is labelled *inferred* in the brief and disclosed up front. Invented social proof, pricing and figures are banned; an undecided fact is recorded as *undecided*, never padded.
20
+
21
+ ## The return
22
+
23
+ A confirmed written brief in the brief block's shape, read back to the person in full and confirmed with one word before it is written into the ship notes. Proposed edits to `docs/project-context.template.md` are listed as a diff for the owner, not applied by this playbook. Then hand off: a new marketing surface goes to `design-taste-frontend`, a working surface to the `/design` loop, and a change to an existing one to the playbook the brief names.
@@ -0,0 +1,24 @@
1
+ # `impeccable typeset` — type on a role scale, on the pack's faces
2
+
3
+ **When:** type feels off: sizes that do not relate, a headline that wraps to three lines, prose that runs wide, numbers that jitter. Edits: yes, on the type tokens and the rules that read them.
4
+
5
+ **Runs in:** every case. The faces are the pack's two stacks in every case; a brief that names another typeface gets the nearest face the pack carries and a line in the notes.
6
+
7
+ ## The role scale
8
+
9
+ Name the roles the surface uses before setting a size: **display, headline, title, body, label, caption, data**. Each maps to one token on `body` (size, line-height, weight, tracking, and the face it takes from the two stacks). A size that belongs to no role is deleted. The world's hierarchy caps the count; the chrome world's display voice is light, tight-tracked and lowercase, its headline holds two lines, its labels are a mono face and are the only place it shouts.
10
+
11
+ - **Measure**: prose 65 to 75 characters (`max-width` in `ch`), and the measure is spent on the reading text, not on a container.
12
+ - **The display ceiling**: a `clamp()` whose upper bound is the world's, and a tracking floor at display sizes so a big face does not fall apart.
13
+ - **Balance**: `text-wrap: balance` on headings, `pretty` on prose, and no orphaned last word on a headline at 1440 or 390.
14
+ - **Data**: `font-variant-numeric: tabular-nums` on anything a ledger produced, so a live refresh does not reflow the row.
15
+ - **Loading**: `font-display: swap` with a fallback sized to the web face (`size-adjust`, `ascent-override`) so nothing shifts when it arrives.
16
+ - **Case**: sentence case for headlines and prose, the world's lowercase for its display voice and controls, uppercase only in a label face the world names.
17
+
18
+ ## Verify from computed values
19
+
20
+ The kit's shots at 1440 / 390 / 320 with real copy from the fixtures: no overflow, the headline within the world's line count, the measure within range, the bar on one row. Measure the rendered line length, do not estimate it.
21
+
22
+ ## The return
23
+
24
+ The role tokens declared once; every text rule reading a role; the shots clean at all three widths in both modes; the ship notes list any role the world's `DESIGN.md` does not name, as a gap for the owner rather than a size this page invented.
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.461",
3
+ "version": "1.19.463",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.461",
9
+ "version": "1.19.463",
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.461",
3
+ "version": "1.19.463",
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.461'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
36
+ const CORE_VERSION = '1.19.463'; // 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');
@@ -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
+ });
@@ -107,6 +107,47 @@ test('the taste core of task 1003325 exists as four in-house skills and every on
107
107
  }
108
108
  });
109
109
 
110
+ // ── the impeccable method (task 1003326, ADR 0221): one skill, one playbook per sub-command ──
111
+ const IMPECCABLE = path.join(SKILLS, 'impeccable');
112
+ const IMPECCABLE_SUBCOMMANDS = ['shape', 'audit', 'critique', 'polish', 'harden', 'animate', 'typeset', 'colorize', 'layout', 'adapt', 'distill', 'clarify', 'onboard', 'optimize', 'document', 'extract'];
113
+
114
+ test('impeccable is one in-house skill whose sixteen sub-commands each own exactly one playbook under reference/', () => {
115
+ assert.ok(ownDirs.includes('impeccable'), 'skills/impeccable/SKILL.md exists');
116
+ assert.ok(manifest.contributes.skills.includes('impeccable'), 'declared in contributes.skills');
117
+ const skill = read(path.join(IMPECCABLE, 'SKILL.md'));
118
+ const refDir = path.join(IMPECCABLE, 'reference');
119
+ const onDisk = fs.readdirSync(refDir).filter((f) => f.endsWith('.md')).map((f) => f.replace(/\.md$/, '')).sort();
120
+ for (const sub of IMPECCABLE_SUBCOMMANDS) {
121
+ assert.ok(onDisk.includes(sub), `reference/${sub}.md exists (the task body names it)`);
122
+ assert.ok(skill.includes(`reference/${sub}.md`), `SKILL.md routes \`${sub}\` to its playbook`);
123
+ }
124
+ for (const ref of onDisk) assert.ok(skill.includes(`reference/${ref}.md`), `reference/${ref}.md is routed from SKILL.md (no orphan playbook)`);
125
+ for (const ref of onDisk) {
126
+ const text = read(path.join(refDir, `${ref}.md`));
127
+ assert.ok(!text.includes(policy.preamble.begin), `reference/${ref}.md carries no preamble copy (the block is pinned once, in SKILL.md, and governs every playbook)`);
128
+ assert.ok(text.trim().length > 400, `reference/${ref}.md is a playbook, not a stub`);
129
+ }
130
+ });
131
+
132
+ test('impeccable carries none of the behaviour that made its origin dangerous: no hooks, no bypass agent, no telemetry, no remote bundle, no live-server token', () => {
133
+ const files = [path.join(IMPECCABLE, 'SKILL.md'), ...fs.readdirSync(path.join(IMPECCABLE, 'reference')).map((f) => path.join(IMPECCABLE, 'reference', f))];
134
+ for (const f of files) {
135
+ const text = read(f);
136
+ const rel = path.relative(ROOT, f).split(path.sep).join('/');
137
+ assert.doesNotMatch(text, /bypassPermissions|--dangerously-skip-permissions/, `${rel}: no permission bypass`);
138
+ assert.doesNotMatch(text, /\b(write|add|install|register)\b[^.\n]{0,60}\b(PostToolUse|PreToolUse|Stop)\b[^.\n]{0,40}\bhooks?\b/i, `${rel}: no step writes a hook into a harness config`);
139
+ assert.doesNotMatch(text, /\b(write|edit|patch)\b[^.\n]{0,40}\bsettings(\.local)?\.json\b/i, `${rel}: no step edits settings.json`);
140
+ assert.doesNotMatch(text, /\b(enable|send|collect|report|emit)s?\b[^.\n]{0,40}\btelemetry\b|\btelemetry\b[^.\n]{0,40}\b(is|are|stays|defaults?)\s+(on|enabled|sent)\b/i, `${rel}: no step turns on or sends telemetry (naming the ban is fine; directing it is not)`);
141
+ assert.doesNotMatch(text, /\b(curl|wget|fetch|download)\b[^.\n]{0,60}\bhttps?:\/\//i, `${rel}: no remote bundle download`);
142
+ assert.doesNotMatch(text, /\b(auth|access|bearer|helper)\s+token\b/i, `${rel}: no live-server token`);
143
+ assert.doesNotMatch(text, /(disregard|ignore)\b[^.\n]{0,40}\b(system prompt|harness)/i, `${rel}: no instruction-override directive`);
144
+ }
145
+ const skill = read(path.join(IMPECCABLE, 'SKILL.md'));
146
+ assert.match(skill, /playwright/i, 'the live loop is the playwright plugin, named as a prerequisite');
147
+ assert.match(skill, /kit\/render\.js/, 'the headless loop is the kit');
148
+ assert.match(skill, /1003329/, 'the detector is named as its own task, not shipped here');
149
+ });
150
+
110
151
  test('every in-house SKILL.md opens with the PLATFORM PREAMBLE, byte-identical to PREAMBLE.md, exactly once, right after its frontmatter', () => {
111
152
  const pre = read(path.join(SKILLS, 'PREAMBLE.md')).trimEnd();
112
153
  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
- }());