@bongos/core 1.19.116 → 1.19.118
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +42 -27
- package/clients/bongos-client/index.d.ts +2 -2
- package/config/design-tokens.neutral.json +13 -13
- package/docs/api/openapi.json +6 -0
- package/docs/api-reference.md +2 -2
- package/docs/design-contract.md +3 -1
- package/modules/platform-identity/migrations/platform_identity_006_project_type.sql +23 -0
- package/modules/platform-identity/platform-identity.js +20 -10
- package/modules/platform-identity/routes/projects.js +2 -0
- package/modules/platform-identity/tests/platform-identity.mjs +14 -0
- package/modules/provisioning/migrations/provisioning_007_project_type.sql +24 -0
- package/modules/provisioning/provisioning.js +17 -5
- package/modules/provisioning/routes/provisioning.js +14 -0
- package/modules/provisioning/tests/provisioning.mjs +7 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +5 -1
- package/public-landing/projects.html +150 -34
- package/scripts/gds/provision.js +3 -1
- package/src/module-api.js +5 -1
- package/src/ui/adapters/claude-design/index.js +14 -2
- package/tests/cosmos_token_contrast.mjs +7 -0
- package/tests/design_tokens_sync.mjs +79 -0
- package/tests/projects_hub.mjs +42 -11
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// tests/design_tokens_sync.mjs — config/design-tokens.neutral.json mirrors the
|
|
2
|
+
// branding pack, never a palette of its own (task 1002752).
|
|
3
|
+
//
|
|
4
|
+
// The repo has two files describing the neutral look: config/branding.neutral.json
|
|
5
|
+
// (the RUNTIME pack — themeMarkup() paints every served page with it) and
|
|
6
|
+
// config/design-tokens.neutral.json (the DESIGN-side tokens feeding the Figma /
|
|
7
|
+
// Claude Design round-trip). Nothing tied them together, so the design file kept
|
|
8
|
+
// the pre-1002749 palette — including the exact accents removed for failing WCAG
|
|
9
|
+
// AA — and every prototype started from colours the product no longer paints.
|
|
10
|
+
// Model: the fallback-equals-pack assertions in tests/devbox_dmg_backdrop.mjs.
|
|
11
|
+
//
|
|
12
|
+
// Run: node tests/design_tokens_sync.mjs
|
|
13
|
+
|
|
14
|
+
import { strict as assert } from 'node:assert';
|
|
15
|
+
import { test } from 'node:test';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
const readJson = (...p) => JSON.parse(fs.readFileSync(path.join(REPO_ROOT, ...p), 'utf8'));
|
|
22
|
+
|
|
23
|
+
const pack = readJson('config', 'branding.neutral.json');
|
|
24
|
+
const tokens = readJson('config', 'design-tokens.neutral.json');
|
|
25
|
+
|
|
26
|
+
// Strip comments so rationale prose — which must be free to name a removed
|
|
27
|
+
// colour — never trips a scan of the CODE (the stripCommentsForIdentity
|
|
28
|
+
// contract, as applied in tests/devbox_dmg_backdrop.mjs).
|
|
29
|
+
function codeOnly(text) {
|
|
30
|
+
return text
|
|
31
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
32
|
+
.split(/\r?\n/)
|
|
33
|
+
.map((line) => line.replace(/(^|[^:"'])\/\/.*$/, '$1'))
|
|
34
|
+
.join('\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test('every design-token colour is the branding pack value — a divergent token is a fork of the brand', () => {
|
|
38
|
+
const packColors = pack.theme.ui.colors;
|
|
39
|
+
assert.deepEqual(
|
|
40
|
+
Object.keys(tokens.colors || {}).sort(), Object.keys(packColors).sort(),
|
|
41
|
+
'the design file must carry exactly the pack colour keys — a missing or extra key is drift too, ' +
|
|
42
|
+
'running through the key set instead of the values');
|
|
43
|
+
for (const [key, val] of Object.entries(tokens.colors)) {
|
|
44
|
+
assert.equal(String(val).toLowerCase(), String(packColors[key]).toLowerCase(),
|
|
45
|
+
`design-token "${key}" must equal the pack's value — a drifted token hands every prototype ` +
|
|
46
|
+
'a palette the product no longer paints');
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('the fonts the two files share are the same fonts', () => {
|
|
51
|
+
const packFonts = pack.theme.ui.fonts;
|
|
52
|
+
for (const [key, val] of Object.entries(tokens.fonts || {})) {
|
|
53
|
+
if (!(key in packFonts)) continue; // mono is design-side only — the pack declares no mono surface
|
|
54
|
+
assert.deepEqual(val, packFonts[key],
|
|
55
|
+
`design-token font "${key}" must equal the pack's — same drift class as the colours`);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('the claude-design adapter falls back to the pack accent, never a brand literal of its own', () => {
|
|
60
|
+
const source = fs.readFileSync(
|
|
61
|
+
path.join(REPO_ROOT, 'src', 'ui', 'adapters', 'claude-design', 'index.js'), 'utf8');
|
|
62
|
+
const block = source.match(/const FALLBACK_ACCENT =[\s\S]*?\}\)\(\);/);
|
|
63
|
+
assert.ok(block, 'the adapter declares one resolved fallback accent instead of inline literals');
|
|
64
|
+
assert.match(block[0], /design-tokens\.neutral\.json/,
|
|
65
|
+
'the fallback resolves from the neutral token file, never a bare literal');
|
|
66
|
+
for (const hex of block[0].match(/#[0-9a-fA-F]{6}\b/g) || []) {
|
|
67
|
+
assert.equal(hex.toLowerCase(), String(pack.theme.ui.colors.accent).toLowerCase(),
|
|
68
|
+
'the catch literal must be the pack ACCENT, not merely some pack colour');
|
|
69
|
+
}
|
|
70
|
+
// Every colour literal left in the adapter must be the neutral pack's own value
|
|
71
|
+
// (i.e. a fallback, not a brand). The preview-card chrome is 3-digit grays, so
|
|
72
|
+
// a 6-digit literal is exactly the brand-value class.
|
|
73
|
+
const packValues = new Set(Object.values(pack.theme.ui.colors).map((h) => String(h).toLowerCase()));
|
|
74
|
+
const hexes = codeOnly(source).match(/#[0-9a-fA-F]{6}\b/g) || [];
|
|
75
|
+
for (const hex of hexes) {
|
|
76
|
+
assert.ok(packValues.has(hex.toLowerCase()),
|
|
77
|
+
`hardcoded colour ${hex} in the claude-design adapter — resolve it from the tokens instead`);
|
|
78
|
+
}
|
|
79
|
+
});
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -228,11 +228,37 @@ test('/create is absorbed — the wizard lives here and the route only redirects
|
|
|
228
228
|
const server = fs.readFileSync(path.join(ROOT, 'src', 'platform-server.js'), 'utf8');
|
|
229
229
|
assert.match(server, /res\.redirect\(302, '\/projects\?view=new'/,
|
|
230
230
|
'/create must 302 into the hub (302, not 301: the URL is an OAuth return target in the wild)');
|
|
231
|
-
for (const id of ['panel-gate', 'panel-name', 'panel-home', 'panel-address', 'panel-review', 'panel-done']) {
|
|
231
|
+
for (const id of ['panel-gate', 'panel-name', 'panel-type', 'panel-home', 'panel-address', 'panel-review', 'panel-done']) {
|
|
232
232
|
assert.match(HUB, new RegExp(`id="${id}"`), `the wizard's ${id} ships as real markup (C7)`);
|
|
233
233
|
}
|
|
234
234
|
});
|
|
235
235
|
|
|
236
|
+
test('the wizard step indices stay coupled — data-next lands one past its own panel (task 1002772)', () => {
|
|
237
|
+
// The whole renumbering turns on validateStep(next - 1): a form's data-next
|
|
238
|
+
// must sit inside panels[next - 1], or the validator guards the wrong step.
|
|
239
|
+
const pm = SCRIPT.match(/var panels = \{([^}]*)\}/);
|
|
240
|
+
assert.ok(pm, 'the panels map must exist');
|
|
241
|
+
const map = {};
|
|
242
|
+
for (const m of pm[1].matchAll(/(\d+):\s*'([a-z-]+)'/g)) map[Number(m[1])] = m[2];
|
|
243
|
+
const nexts = [...HUB.matchAll(/data-next="(\d+)"/g)];
|
|
244
|
+
assert.ok(nexts.length >= 4, 'every wizard form declares its next step');
|
|
245
|
+
for (const m of nexts) {
|
|
246
|
+
const before = HUB.slice(0, m.index);
|
|
247
|
+
const sec = before.lastIndexOf('id="panel-');
|
|
248
|
+
assert.ok(sec !== -1, 'a data-next form lives inside a panel');
|
|
249
|
+
const id = HUB.slice(sec + 4).match(/^(panel-[a-z]+)/)[1];
|
|
250
|
+
assert.equal(id, map[Number(m[1]) - 1],
|
|
251
|
+
`data-next="${m[1]}" must live in panels[${Number(m[1]) - 1}] — validateStep(next-1) guards that panel`);
|
|
252
|
+
}
|
|
253
|
+
for (const m of HUB.matchAll(/data-back="(\d+)"/g)) {
|
|
254
|
+
assert.ok(map[Number(m[1])], `data-back="${m[1]}" must point at a mapped panel`);
|
|
255
|
+
}
|
|
256
|
+
// the rail numbers every step except the gate and the done panel
|
|
257
|
+
const beats = [...HUB.matchAll(/data-step="(\d+)"/g)].map((x) => Number(x[1]));
|
|
258
|
+
assert.equal(Math.max.apply(null, beats), Object.keys(map).length - 2,
|
|
259
|
+
'the rail shows every numbered step except gate and done');
|
|
260
|
+
});
|
|
261
|
+
|
|
236
262
|
test('the absorbed wizard sheds its two URL landmines — /gate/ and the whole-query strip', () => {
|
|
237
263
|
// create.html carried a document-level Escape that navigated to the retired
|
|
238
264
|
// /gate/ (a 404 the href-only guard above cannot see: it was a
|
|
@@ -475,18 +501,21 @@ test('the row keeps its own Open link working underneath the card', () => {
|
|
|
475
501
|
The mock's columns shipped whole — Project / Builders / Type / Tags / Status
|
|
476
502
|
— but three of them rendered an em dash for every row, forever, because the
|
|
477
503
|
public projection carries four fields and Type/Tags are not in the catalog
|
|
478
|
-
schema at all. A header promising a column is a claim like any other.
|
|
504
|
+
schema at all. A header promising a column is a claim like any other.
|
|
505
|
+
Builders and Runs came back with a source in task 1002771; Type came back in
|
|
506
|
+
task 1002772 when a project began DECLARING it at creation — a nullable
|
|
507
|
+
declared field, so an em dash now means "not declared", never a guess. ── */
|
|
479
508
|
|
|
480
|
-
test('
|
|
509
|
+
test('every table column has a source — Type is declared, never guessed', () => {
|
|
481
510
|
const thead = (HUB.match(/<thead>([\s\S]*?)<\/thead>/) || [, ''])[1];
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
assert.doesNotMatch(thead, />Type</, 'Type has no source until a project declares it');
|
|
485
|
-
// Builders and Runs came back in task 1002771 WITH a source behind them
|
|
511
|
+
assert.match(thead, />Type</, 'Type has a source now: the project declares it at creation (task 1002772)');
|
|
512
|
+
assert.match(SCRIPT, /typeHtml\(p\.type\)/, 'the Type cell must read the declared field through the one formatter');
|
|
486
513
|
assert.match(thead, />Builders</);
|
|
487
514
|
assert.match(thead, />Runs</);
|
|
488
515
|
assert.match(SCRIPT, /p\.builders/, 'the Builders cell must read real data');
|
|
489
516
|
assert.match(SCRIPT, /tagsHtml\(p\.tags\)/, 'the Runs cell must read real data');
|
|
517
|
+
// the wizard files the declaration with the create — the source end of the pipe
|
|
518
|
+
assert.match(SCRIPT, /type: state\.projType/, 'the create request carries the declared type');
|
|
490
519
|
});
|
|
491
520
|
|
|
492
521
|
test('header and body agree on the column count', () => {
|
|
@@ -509,11 +538,13 @@ test('no sort control is offered for a column that cannot sort', () => {
|
|
|
509
538
|
}
|
|
510
539
|
});
|
|
511
540
|
|
|
512
|
-
test('
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
assert.ok(COSMOS.includes(live), `${live} styles a column that now renders real data`);
|
|
541
|
+
test('every live column keeps its styling — .colType included since task 1002772', () => {
|
|
542
|
+
for (const live of ['.tbl .num', '.tbl .tags', '.colTags', '.colType', '.typeTag']) {
|
|
543
|
+
assert.ok(COSMOS.includes(live), `${live} styles a column that renders real data`);
|
|
516
544
|
}
|
|
545
|
+
// the type tag is its own vocabulary: not the relationship .pill, not the
|
|
546
|
+
// liveness .chip — two different facts must not wear the same chrome
|
|
547
|
+
assert.doesNotMatch(SCRIPT, /typeHtml[\s\S]{0,80}?class="pill"/, 'type must not wear the relationship pill');
|
|
517
548
|
});
|
|
518
549
|
|
|
519
550
|
/* ── Real Builders + Runs (task 1002771) ────────────────────────────────────
|