@bongos/core 1.19.140 → 1.19.142
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 +12 -12
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +17 -6
- package/public-landing/projects.html +34 -5
- package/src/module-api.js +5 -1
- package/tests/cosmos_token_contrast.mjs +131 -7
- package/tests/public_landing_projects.mjs +75 -0
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.142",
|
|
6
|
+
"core_contract": "1.19.142",
|
|
7
|
+
"source_commit": "34ee06f88922e1b277d1d8780b0da9c9fa7edd2a",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-15T15:
|
|
9
|
+
"built_at": "2026-08-15T15:39:26.241Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 303,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1574,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "c544fb023102eaf104fa988b33f2cb9c70a2d8817edf0549e3e5608ba47be6e4",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -4177,12 +4177,12 @@
|
|
|
4177
4177
|
{
|
|
4178
4178
|
"path": "package-lock.json",
|
|
4179
4179
|
"mode": "0000644",
|
|
4180
|
-
"sha256": "
|
|
4180
|
+
"sha256": "9c082e0279aa72767e3167df6277a3f34247a7d600bc184f00a4ca9380b97da5"
|
|
4181
4181
|
},
|
|
4182
4182
|
{
|
|
4183
4183
|
"path": "package.json",
|
|
4184
4184
|
"mode": "0000644",
|
|
4185
|
-
"sha256": "
|
|
4185
|
+
"sha256": "b3d152773025a6478d933888ade55e094a55016f8260989361ef426838700d03"
|
|
4186
4186
|
},
|
|
4187
4187
|
{
|
|
4188
4188
|
"path": "public-docs/index.html",
|
|
@@ -4422,7 +4422,7 @@
|
|
|
4422
4422
|
{
|
|
4423
4423
|
"path": "public-landing/assets/cosmos.css",
|
|
4424
4424
|
"mode": "0000644",
|
|
4425
|
-
"sha256": "
|
|
4425
|
+
"sha256": "0e023e2e98babfd7d748b2f7542af13c41be96cbcc84f2853110410a57fe7816"
|
|
4426
4426
|
},
|
|
4427
4427
|
{
|
|
4428
4428
|
"path": "public-landing/buddha.png",
|
|
@@ -4437,7 +4437,7 @@
|
|
|
4437
4437
|
{
|
|
4438
4438
|
"path": "public-landing/projects.html",
|
|
4439
4439
|
"mode": "0000644",
|
|
4440
|
-
"sha256": "
|
|
4440
|
+
"sha256": "eb36261b21198d9217d96408de5ee898083aaf0d5f62368f1be3b4af9a908dcb"
|
|
4441
4441
|
},
|
|
4442
4442
|
{
|
|
4443
4443
|
"path": "public-landing/world/assets/clouds/cloud-island-large.png",
|
|
@@ -5767,7 +5767,7 @@
|
|
|
5767
5767
|
{
|
|
5768
5768
|
"path": "src/module-api.js",
|
|
5769
5769
|
"mode": "0000644",
|
|
5770
|
-
"sha256": "
|
|
5770
|
+
"sha256": "6eaa713a395bf793d4e07c0d68ae86a29473e410d393b859dcfd1d52bacb2811"
|
|
5771
5771
|
},
|
|
5772
5772
|
{
|
|
5773
5773
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6277,7 +6277,7 @@
|
|
|
6277
6277
|
{
|
|
6278
6278
|
"path": "tests/cosmos_token_contrast.mjs",
|
|
6279
6279
|
"mode": "0000644",
|
|
6280
|
-
"sha256": "
|
|
6280
|
+
"sha256": "15ebf10ea627a32e943d139f1a713ff46cfda14fa1a39a12d87bcc78934516c0"
|
|
6281
6281
|
},
|
|
6282
6282
|
{
|
|
6283
6283
|
"path": "tests/cost.mjs",
|
|
@@ -7352,7 +7352,7 @@
|
|
|
7352
7352
|
{
|
|
7353
7353
|
"path": "tests/public_landing_projects.mjs",
|
|
7354
7354
|
"mode": "0000644",
|
|
7355
|
-
"sha256": "
|
|
7355
|
+
"sha256": "d7998f770254f2c0179a6269d408f571c8757a97defd2543462b8496cb5f3e23"
|
|
7356
7356
|
},
|
|
7357
7357
|
{
|
|
7358
7358
|
"path": "tests/public_mirror_export.mjs",
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.142",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.142",
|
|
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.
|
|
3
|
+
"version": "1.19.142",
|
|
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",
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
--ink:#fdeedd; --ink-soft:#dcb693; --ink-faint:#b89272;
|
|
99
99
|
--rule:#5e3521; --rule-soft:#3f2317;
|
|
100
100
|
--accent-deep:color-mix(in srgb, var(--accent) 45%, #ffffff);
|
|
101
|
+
--link-ink:color-mix(in srgb, var(--accent) 51%, #ffffff);
|
|
101
102
|
--sea-ink:color-mix(in srgb, var(--sea) 65%, #ffffff);
|
|
102
103
|
--ok:#4ade80; --warn:#fbbf24; --err:#f87171;
|
|
103
104
|
--on-ok:#0b2e18;
|
|
@@ -125,6 +126,7 @@
|
|
|
125
126
|
--ink:#fdeedd; --ink-soft:#dcb693; --ink-faint:#b89272;
|
|
126
127
|
--rule:#5e3521; --rule-soft:#3f2317;
|
|
127
128
|
--accent-deep:color-mix(in srgb, var(--accent) 45%, #ffffff);
|
|
129
|
+
--link-ink:color-mix(in srgb, var(--accent) 51%, #ffffff);
|
|
128
130
|
--sea-ink:color-mix(in srgb, var(--sea) 65%, #ffffff);
|
|
129
131
|
--ok:#4ade80; --warn:#fbbf24; --err:#f87171;
|
|
130
132
|
--on-ok:#0b2e18;
|
|
@@ -160,6 +162,15 @@
|
|
|
160
162
|
--sea as-is; dark derives brighter (the --accent-deep pattern) so a pack's
|
|
161
163
|
light-tuned sea clears WCAG 1.4.11's 3:1 on the near-black card too */
|
|
162
164
|
--sea-ink:var(--sea);
|
|
165
|
+
/* TEXT LINKS ONLY (task 1002822). Light is --accent-deep unchanged; dark needs
|
|
166
|
+
its own mix because --accent-deep and --ink-soft land 2 lightness points
|
|
167
|
+
apart there (#e9ad8f vs #dcb693, ΔE76 9.7) — the same colour, and dark links
|
|
168
|
+
carry no underline until hover, so weight was the only cue left. 51% is the
|
|
169
|
+
ceiling, not a preference: --accent-deep can't simply be pushed (it also
|
|
170
|
+
feeds --chrome-accent/--glass-ink/--hero-b/--foot-code), and past 51% a dark
|
|
171
|
+
navy pack accent falls under 4.5:1 on --bg-deep. Chrome keeps --accent-deep;
|
|
172
|
+
only rules that paint a link inside prose take this. */
|
|
173
|
+
--link-ink:var(--accent-deep);
|
|
163
174
|
--neutral-soft:color-mix(in srgb, var(--ink) 8%, var(--bg-card));
|
|
164
175
|
/* ink-derived so the shadow is a WARM brown on the marigold ground instead of
|
|
165
176
|
the cold navy it used to cast. The dark blocks override it with a plain black
|
|
@@ -211,7 +222,7 @@ body{font-family:var(--font-body);color:var(--ink);background:var(--page-bg);min
|
|
|
211
222
|
.card{background:var(--card);border-radius:26px;box-shadow:var(--shadow-card);padding:clamp(22px,3.6vw,40px);}
|
|
212
223
|
h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32px);letter-spacing:-0.5px;line-height:1.15;margin-bottom:8px;}
|
|
213
224
|
.lede{font-size:15.5px;color:var(--ink-soft);font-weight:500;line-height:1.55;margin-bottom:24px;max-width:62ch;}
|
|
214
|
-
.lede a{color:var(--
|
|
225
|
+
.lede a{color:var(--link-ink);font-weight:700;text-decoration:none;}
|
|
215
226
|
.lede a:hover{text-decoration:underline;}
|
|
216
227
|
|
|
217
228
|
/* ── buttons ── */
|
|
@@ -225,7 +236,7 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
225
236
|
.btn.danger{background:transparent;color:var(--err);box-shadow:none;border:2px solid color-mix(in srgb,var(--err) 35%,transparent);}
|
|
226
237
|
.btn.danger:hover{background:var(--err-soft);border-color:var(--err);}
|
|
227
238
|
.btn .gh{width:17px;height:17px;fill:currentColor;}
|
|
228
|
-
.linklike{background:none;border:none;cursor:pointer;font-family:var(--font-body);font-weight:700;font-size:13.5px;color:var(--
|
|
239
|
+
.linklike{background:none;border:none;cursor:pointer;font-family:var(--font-body);font-weight:700;font-size:13.5px;color:var(--link-ink);text-decoration:underline;padding:6px 4px;}
|
|
229
240
|
|
|
230
241
|
/* ── chips: the one status vocabulary (direction §4). Recency rides inside
|
|
231
242
|
(.rec) — "live" always says when it was last verified. ── */
|
|
@@ -271,7 +282,7 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
271
282
|
.projMeta{font-size:13px;font-weight:600;color:var(--ink-faint);margin-top:6px;display:flex;gap:14px;flex-wrap:wrap;}
|
|
272
283
|
.projMeta code{font-family:var(--font-mono);font-size:12.5px;background:var(--bg-deep);border-radius:6px;padding:1px 6px;color:var(--accent-deep);}
|
|
273
284
|
.addr{margin-top:12px;font-size:14.5px;font-weight:600;color:var(--ink-soft);}
|
|
274
|
-
.addr a{color:var(--
|
|
285
|
+
.addr a{color:var(--link-ink);font-weight:700;text-decoration:none;font-family:var(--font-mono);font-size:13.5px;}
|
|
275
286
|
.addr a:hover{text-decoration:underline;}
|
|
276
287
|
.addr .none{color:var(--ink-faint);font-style:italic;}
|
|
277
288
|
.addr code{font-family:var(--font-mono);font-size:13px;background:var(--bg-deep);border-radius:6px;padding:1px 7px;color:var(--accent-deep);}
|
|
@@ -282,7 +293,7 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
282
293
|
.projMsg{font-size:13.5px;font-weight:600;margin-top:10px;min-height:18px;color:var(--ink-soft);line-height:1.5;}
|
|
283
294
|
.projMsg.ok{color:var(--ok);}
|
|
284
295
|
.projMsg.err{color:var(--err);}
|
|
285
|
-
.projMsg a{color:var(--
|
|
296
|
+
.projMsg a{color:var(--link-ink);font-weight:700;text-decoration:none;}
|
|
286
297
|
.projMsg a:hover{text-decoration:underline;}
|
|
287
298
|
.projMsg details{margin-top:8px;}
|
|
288
299
|
.projMsg summary{cursor:pointer;font-size:12.5px;font-weight:700;color:var(--ink-faint);}
|
|
@@ -293,7 +304,7 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
293
304
|
.finePrint{font-size:12.5px;color:var(--ink-faint);font-weight:600;margin-top:16px;line-height:1.5;}
|
|
294
305
|
/* a link in fine print still has to look like one — without this it fell back to
|
|
295
306
|
the browser's default blue, the one colour in the page that is not ours */
|
|
296
|
-
.finePrint a{color:var(--
|
|
307
|
+
.finePrint a{color:var(--link-ink);font-weight:700;text-decoration:none;}
|
|
297
308
|
.finePrint a:hover{text-decoration:underline;}
|
|
298
309
|
|
|
299
310
|
/* ── progress strips: one visual, per-flow vocabularies (direction §4).
|
|
@@ -315,7 +326,7 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
315
326
|
.strip .slbl{font-size:12px;font-weight:700;color:var(--ink-faint);}
|
|
316
327
|
.strip .stages li.now .slbl,.strip .stages li.done .slbl{color:color-mix(in srgb,var(--sea) 40%,var(--ink));}
|
|
317
328
|
.strip .pnote{text-align:center;font-size:13px;font-weight:600;color:var(--ink-soft);min-height:18px;line-height:1.5;}
|
|
318
|
-
.strip .pnote a{color:var(--
|
|
329
|
+
.strip .pnote a{color:var(--link-ink);font-weight:700;text-decoration:none;}
|
|
319
330
|
.strip.isLive{border-color:var(--ok-soft);background:linear-gradient(180deg,color-mix(in srgb,var(--ok) 8%,var(--bg-card)) 0%,var(--bg-card) 100%);}
|
|
320
331
|
.strip.isLive .stages li.done:not(:first-child)::before{background:var(--ok);}
|
|
321
332
|
.strip.isLive .stages li.done .sdot{border-color:var(--ok);background:var(--ok);}
|
|
@@ -159,6 +159,10 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
159
159
|
}
|
|
160
160
|
.humanSteps b{font-size:15px;}
|
|
161
161
|
.humanSteps p{font-size:13.5px;font-weight:500;color:var(--ink-soft);line-height:1.55;margin-top:4px;}
|
|
162
|
+
/* a repo the platform already handled is named as a plain inline link (task 1002855) —
|
|
163
|
+
the .ghbtn treatment reads as an action to take, and there is nothing here to do */
|
|
164
|
+
.humanSteps p a{color:var(--link-ink);font-weight:700;text-decoration:none;}
|
|
165
|
+
.humanSteps p a:hover{text-decoration:underline;}
|
|
162
166
|
/* live checklist states (task 1002694): a step the platform has verified done, and a
|
|
163
167
|
step that can't be performed yet (its prerequisite — usually the domain — is missing) */
|
|
164
168
|
.humanSteps>li.sdone{border-color:color-mix(in srgb,var(--ok) 30%,var(--bg-card));background:linear-gradient(180deg,color-mix(in srgb,var(--ok) 8%,var(--bg-card)) 0%,var(--bg-card) 100%);}
|
|
@@ -218,7 +222,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
218
222
|
.prog .slbl{font-size:12px;font-weight:700;color:var(--ink-faint);}
|
|
219
223
|
.prog .stages li.now .slbl,.prog .stages li.done .slbl{color:color-mix(in srgb,var(--sea) 40%,var(--ink));}
|
|
220
224
|
.prog .pnote{text-align:center;font-size:13px;font-weight:600;color:color-mix(in srgb,var(--sea) 45%,var(--ink-soft));min-height:18px;}
|
|
221
|
-
.prog .pnote a{color:var(--
|
|
225
|
+
.prog .pnote a{color:var(--link-ink);font-weight:700;text-decoration:none;}
|
|
222
226
|
.prog .pnote a:hover{text-decoration:underline;}
|
|
223
227
|
.prog.isLive{background:linear-gradient(180deg,color-mix(in srgb,var(--ok) 8%,var(--bg-card)) 0%,var(--bg-card) 100%);border-color:color-mix(in srgb,var(--ok) 30%,var(--bg-card));}
|
|
224
228
|
.prog.isLive .stages li.done:not(:first-child)::before{background:var(--ok);}
|
|
@@ -2154,6 +2158,19 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
2154
2158
|
if (owner) u.searchParams.set('owner', owner);
|
|
2155
2159
|
return u.toString();
|
|
2156
2160
|
}
|
|
2161
|
+
/* github.com address of the repo this draft points at, or '' when we can't name one
|
|
2162
|
+
honestly. Adopt: owner/repo IS the address the owner picked. Auto-create: the
|
|
2163
|
+
html_url the create call returned — a draft predating task 1002855 has none, and
|
|
2164
|
+
gets no link rather than a guess that could 404. The scheme is hard-coded and both
|
|
2165
|
+
segments are encoded, so nothing here can become an off-site or javascript: href. */
|
|
2166
|
+
function repoLinkFor() {
|
|
2167
|
+
if (!state.owner || !state.repo) return '';
|
|
2168
|
+
if (state.mode === 'adopt') {
|
|
2169
|
+
return 'https://github.com/' + encodeURIComponent(state.owner) + '/' + encodeURIComponent(state.repo);
|
|
2170
|
+
}
|
|
2171
|
+
var stored = String(state.repoUrl || '');
|
|
2172
|
+
return stored.indexOf('https://github.com/') === 0 ? stored : '';
|
|
2173
|
+
}
|
|
2157
2174
|
function saveDraft() {
|
|
2158
2175
|
try { sessionStorage.setItem(DRAFT_KEY, JSON.stringify(state)); } catch (e) {}
|
|
2159
2176
|
}
|
|
@@ -2554,6 +2571,10 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
2554
2571
|
state.owner = full.slice(0, full.indexOf('/'));
|
|
2555
2572
|
state.repo = full.slice(full.indexOf('/') + 1);
|
|
2556
2573
|
}
|
|
2574
|
+
/* Keep GitHub's own html_url so the done panel can link the repo it made
|
|
2575
|
+
(task 1002855). It rides saveDraft(), so a resumed draft keeps the link. */
|
|
2576
|
+
var made = data.repo && data.repo.html_url;
|
|
2577
|
+
if (typeof made === 'string' && made.indexOf('https://github.com/') === 0) state.repoUrl = made;
|
|
2557
2578
|
state.repoAutoCreated = true; saveDraft();
|
|
2558
2579
|
});
|
|
2559
2580
|
});
|
|
@@ -2664,15 +2685,23 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
2664
2685
|
function renderStepGithubRepo() {
|
|
2665
2686
|
var landed = live.instStatus === 'active';
|
|
2666
2687
|
var repoLabel = state.owner && state.repo ? '<strong>' + esc(state.owner + '/' + state.repo) + '</strong>' : 'your new repository';
|
|
2688
|
+
/* Naming a repo without linking it stranded the owner on exactly the path where the
|
|
2689
|
+
platform did the work (task 1002855). Adopt links the repo the owner picked;
|
|
2690
|
+
auto-create links the html_url GitHub itself returned. A draft written before this
|
|
2691
|
+
change carries no stored URL and degrades to the bare name — never a guessed link.
|
|
2692
|
+
Only these two branches link: the manual branch below still has a real .ghbtn
|
|
2693
|
+
action, and nesting an <a> inside it would be malformed. */
|
|
2694
|
+
var repoUrl = repoLinkFor();
|
|
2695
|
+
var repoLink = repoUrl ? '<a href="' + esc(repoUrl) + '" target="_blank" rel="noopener">' + repoLabel + '</a>' : repoLabel;
|
|
2667
2696
|
if (state.mode === 'adopt') {
|
|
2668
2697
|
return landed
|
|
2669
|
-
? '<li class="sdone" data-onboard-step="github-repo"><b>Your repo carries the platform</b><p>The build layered the platform onto ' +
|
|
2670
|
-
: '<li data-onboard-step="github-repo"><b>Your existing repo</b><p>The build clones ' +
|
|
2698
|
+
? '<li class="sdone" data-onboard-step="github-repo"><b>Your repo carries the platform</b><p>The build layered the platform onto ' + repoLink + ' — existing files, history, and backlog kept. Pull to see it.</p></li>'
|
|
2699
|
+
: '<li data-onboard-step="github-repo"><b>Your existing repo</b><p>The build clones ' + repoLink + ' and layers the platform onto its existing history — nothing is created or emptied, and your backlog is imported. Nothing for you to do here.</p></li>';
|
|
2671
2700
|
}
|
|
2672
2701
|
if (state.repoAutoCreated) {
|
|
2673
2702
|
return landed
|
|
2674
|
-
? '<li class="sdone" data-onboard-step="github-repo"><b>Repo ready — scaffold landed</b><p>We created ' +
|
|
2675
|
-
: '<li data-onboard-step="github-repo"><b>Your empty repo is ready</b><p>We created ' +
|
|
2703
|
+
? '<li class="sdone" data-onboard-step="github-repo"><b>Repo ready — scaffold landed</b><p>We created ' + repoLink + ' (public, by design) and the build pushed your project’s starting files to it.</p></li>'
|
|
2704
|
+
: '<li data-onboard-step="github-repo"><b>Your empty repo is ready</b><p>We created ' + repoLink + ' on GitHub for you — public (projects here are open by design), empty, and waiting. The scaffold lands there when the build runs; nothing for you to do here.</p></li>';
|
|
2676
2705
|
}
|
|
2677
2706
|
var ghNewUrl = githubNewRepoUrl(state.owner, state.repo);
|
|
2678
2707
|
return '<li' + (landed ? ' class="sdone"' : '') + ' data-onboard-step="github-repo"><b>Create the empty GitHub repo</b><p>Do this first — the build needs the repo to exist. Click below (GitHub opens with the owner and name already filled in), press <strong>Create repository</strong>, and leave it completely empty. It must land at exactly ' + repoLabel + ' — leave GitHub’s Owner box as prefilled (your personal account), because the build watches that exact address; a repo created under a different owner (an organization, say) is never found. GitHub will ask public or private — your choice; both work here, and we never change it. Seeing “repository not found” above? That just means the repo isn’t created yet — make it here.</p>' +
|
package/src/module-api.js
CHANGED
|
@@ -707,8 +707,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
707
707
|
// landed since 1.19.138 with no explicit bump. run 31891461224. (task 1002620)
|
|
708
708
|
// 1.19.140 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
709
709
|
// landed since 1.19.139 with no explicit bump. run 31891805999. (task 1002620)
|
|
710
|
+
// 1.19.141 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
711
|
+
// landed since 1.19.140 with no explicit bump. run 31892484952. (task 1002620)
|
|
712
|
+
// 1.19.142 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
713
|
+
// landed since 1.19.141 with no explicit bump. run 31893298770. (task 1002620)
|
|
710
714
|
// ---------------------------------------------------------------------------
|
|
711
|
-
const CORE_VERSION = '1.19.
|
|
715
|
+
const CORE_VERSION = '1.19.142'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
712
716
|
|
|
713
717
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
714
718
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -135,6 +135,25 @@ const contrast = (fg, bg) => {
|
|
|
135
135
|
|
|
136
136
|
const hex = (c) => '#' + c.slice(0, 3).map((v) => Math.round(v).toString(16).padStart(2, '0')).join('');
|
|
137
137
|
|
|
138
|
+
// CIE Lab (D65) + ΔE76 — for ink-vs-ink questions contrast() cannot answer.
|
|
139
|
+
// Two colors can each clear 8:1 over the same ground and still BE the same
|
|
140
|
+
// color; a ratio between them only sees lightness, and the collision this file
|
|
141
|
+
// now guards is a chroma one.
|
|
142
|
+
const labF = (t) => (t > 216 / 24389 ? Math.cbrt(t) : (841 / 108) * t + 4 / 29);
|
|
143
|
+
const lab = ([r, g, b]) => {
|
|
144
|
+
const [R, G, B] = [lin(r), lin(g), lin(b)];
|
|
145
|
+
const [fx, fy, fz] = [
|
|
146
|
+
labF((0.4124 * R + 0.3576 * G + 0.1805 * B) / 0.95047),
|
|
147
|
+
labF(0.2126 * R + 0.7152 * G + 0.0722 * B),
|
|
148
|
+
labF((0.0193 * R + 0.1192 * G + 0.9505 * B) / 1.08883),
|
|
149
|
+
];
|
|
150
|
+
return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
|
|
151
|
+
};
|
|
152
|
+
const deltaE76 = (a, b) => {
|
|
153
|
+
const [A, B] = [lab(a), lab(b)];
|
|
154
|
+
return Math.hypot(A[0] - B[0], A[1] - B[1], A[2] - B[2]);
|
|
155
|
+
};
|
|
156
|
+
|
|
138
157
|
// ── the pairs cosmos.css actually paints ─────────────────────────────────────
|
|
139
158
|
// need: 4.5 normal text · 3.0 large text and meaningful non-text
|
|
140
159
|
const CARD = 'var(--bg-card)';
|
|
@@ -143,14 +162,22 @@ const PAIRS = [
|
|
|
143
162
|
['.btn label on its fill', '#ffffff', 'var(--accent)', 4.5],
|
|
144
163
|
['.btn.ghost:hover label', 'var(--accent-deep)', CARD, 4.5],
|
|
145
164
|
['.btn.danger label', 'var(--err)', CARD, 4.5],
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
// dark --sun-lit recolors — they pinned
|
|
149
|
-
// branding pack, so a re-themed instance got
|
|
150
|
-
// The dark blocks derive
|
|
151
|
-
['
|
|
152
|
-
['
|
|
165
|
+
// Code chips, pills, the now-step number, the hover inks: --accent-deep is
|
|
166
|
+
// CHROME ink in both modes since task 1002822 moved the text links off it
|
|
167
|
+
// (task 1002750 had already deleted the dark --sun-lit recolors — they pinned
|
|
168
|
+
// the locked space palette over the branding pack, so a re-themed instance got
|
|
169
|
+
// Cloud Bongos orange links). The dark blocks derive it from --accent.
|
|
170
|
+
['--accent-deep ink on a card', 'var(--accent-deep)', CARD, 4.5],
|
|
171
|
+
['--accent-deep ink on the ground', 'var(--accent-deep)', 'var(--bg)', 4.5],
|
|
153
172
|
['code chip on --bg-deep', 'var(--accent-deep)', 'var(--bg-deep)', 4.5],
|
|
173
|
+
// --link-ink: the text links' own pack-derived token (task 1002822). Nothing
|
|
174
|
+
// measured --accent-deep as a link ink against the copy AROUND it, and in dark
|
|
175
|
+
// the two had collapsed into one color — see the separation test below.
|
|
176
|
+
// --bg-deep is not a link ground today; it is the review panel's robustness
|
|
177
|
+
// floor, so a link dropped into a --bg-deep panel can never ship under AA.
|
|
178
|
+
['--link-ink on a card', 'var(--link-ink)', CARD, 4.5],
|
|
179
|
+
['--link-ink on the page ground', 'var(--link-ink)', 'var(--bg)', 4.5],
|
|
180
|
+
['--link-ink on --bg-deep', 'var(--link-ink)', 'var(--bg-deep)', 4.5],
|
|
154
181
|
// body copy
|
|
155
182
|
['--ink body copy', 'var(--ink)', CARD, 4.5],
|
|
156
183
|
['--ink-soft secondary copy', 'var(--ink-soft)', CARD, 4.5],
|
|
@@ -227,6 +254,103 @@ for (const mode of ['light', 'dark']) {
|
|
|
227
254
|
});
|
|
228
255
|
}
|
|
229
256
|
|
|
257
|
+
// ── ink vs ADJACENT ink ──────────────────────────────────────────────────────
|
|
258
|
+
// Every PAIRS row above is (ink, ground), so the suite was structurally blind to
|
|
259
|
+
// a link that measured 8.74:1 on its card and was STILL the same color as the
|
|
260
|
+
// prose it sat in. That is exactly what dark mode shipped: --accent-deep
|
|
261
|
+
// resolved to #e9ad8f beside --ink-soft #dcb693 — ΔE76 9.7, two lightness points
|
|
262
|
+
// apart — and dark links carry text-decoration:none until hover, so font-weight
|
|
263
|
+
// was the only cue left. The live repro was .projMsg a.
|
|
264
|
+
//
|
|
265
|
+
// ΔE76 and not a luminance ratio, even though contrast() is right there: light
|
|
266
|
+
// mode's ACCEPTED separation is ΔE76 40.9 but only 1.10:1 in luminance. The
|
|
267
|
+
// light pair is told apart by chroma, so a luminance floor would fail the good
|
|
268
|
+
// case and pass the bad one.
|
|
269
|
+
//
|
|
270
|
+
// The floor is 12, not light mode's 40.9: dark's warm ink IS a desaturated
|
|
271
|
+
// ember, and srgb color-mix can only slide the accent along the line toward
|
|
272
|
+
// white, so 51% is the most chroma --link-ink can hold before a dark navy pack
|
|
273
|
+
// accent falls under 4.5:1 on --bg-deep. Shipped: 14.1 dark, 40.9 light.
|
|
274
|
+
const LINK_SEPARATION = 12;
|
|
275
|
+
|
|
276
|
+
test('the link ink never collapses into the copy around it', () => {
|
|
277
|
+
const failures = [];
|
|
278
|
+
for (const mode of ['light', 'dark']) {
|
|
279
|
+
const card = over(resolve(CARD, mode), resolve('var(--page-bg)', mode));
|
|
280
|
+
const link = over(resolve('var(--link-ink)', mode), card);
|
|
281
|
+
for (const ink of ['--ink-soft', '--ink-faint']) {
|
|
282
|
+
const beside = over(resolve(`var(${ink})`, mode), card);
|
|
283
|
+
const got = deltaE76(link, beside);
|
|
284
|
+
if (got < LINK_SEPARATION) {
|
|
285
|
+
failures.push(
|
|
286
|
+
`${mode}: --link-ink ${hex(link)} vs ${ink} ${hex(beside)} — ` +
|
|
287
|
+
`ΔE76 ${got.toFixed(1)} (needs ${LINK_SEPARATION})`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
assert.deepEqual(failures, [], `\n ${failures.join('\n ')}\n`);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// The accent packs the rest of this file never sees. Everything above measures
|
|
296
|
+
// the ONE pack config/branding.neutral.json ships — but --link-ink is DERIVED
|
|
297
|
+
// from --accent in dark, so its tuning has to survive whatever accent a host
|
|
298
|
+
// sets. These four are the review panel's own sweep (task 1002822); each is dark
|
|
299
|
+
// enough to carry white .btn text, which any pack's --accent must already do.
|
|
300
|
+
// DARK only: in light --link-ink is var(--accent-deep), a value the pack ships
|
|
301
|
+
// directly rather than derives, so light is the pack author's own contract.
|
|
302
|
+
const ACCENT_SWEEP = {
|
|
303
|
+
navy: '#1a237e', forest: '#14532d', red: '#b91c1c', magenta: '#a21caf',
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
test('--link-ink survives a re-theme in dark mode', () => {
|
|
307
|
+
const failures = [];
|
|
308
|
+
const shipped = RAW.dark['--accent'];
|
|
309
|
+
try {
|
|
310
|
+
for (const [name, accent] of Object.entries(ACCENT_SWEEP)) {
|
|
311
|
+
RAW.dark['--accent'] = accent;
|
|
312
|
+
const page = resolve('var(--page-bg)', 'dark');
|
|
313
|
+
for (const groundExpr of [CARD, 'var(--bg)', 'var(--bg-deep)']) {
|
|
314
|
+
const bg = over(resolve(groundExpr, 'dark'), page);
|
|
315
|
+
const got = contrast(over(resolve('var(--link-ink)', 'dark'), bg), bg);
|
|
316
|
+
if (got < 4.5) failures.push(`${name} on ${groundExpr}: ${got.toFixed(2)}:1 (needs 4.5)`);
|
|
317
|
+
}
|
|
318
|
+
const card = over(resolve(CARD, 'dark'), page);
|
|
319
|
+
const link = over(resolve('var(--link-ink)', 'dark'), card);
|
|
320
|
+
for (const ink of ['--ink-soft', '--ink-faint']) {
|
|
321
|
+
const got = deltaE76(link, over(resolve(`var(${ink})`, 'dark'), card));
|
|
322
|
+
if (got < LINK_SEPARATION) {
|
|
323
|
+
failures.push(`${name} vs ${ink}: ΔE76 ${got.toFixed(1)} (needs ${LINK_SEPARATION})`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
} finally {
|
|
328
|
+
RAW.dark['--accent'] = shipped;
|
|
329
|
+
}
|
|
330
|
+
assert.deepEqual(failures, [], `\n ${failures.join('\n ')}\n`);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('no text link is left painting --accent-deep', () => {
|
|
334
|
+
// The migration's own regression net. A token only helps the rules that take
|
|
335
|
+
// it, and the rules are spread across two sheets: re-pointing cosmos.css alone
|
|
336
|
+
// left projects.html's own `.humanSteps p a` — a plain inline link sitting in
|
|
337
|
+
// `.humanSteps p{color:var(--ink-soft)}` — on the colliding ink. Anchor
|
|
338
|
+
// selectors only: --accent-deep is still the right ink for the chrome that
|
|
339
|
+
// never sits inside prose (code chips, .tbl/.cardRel pills, .btn.ghost:hover,
|
|
340
|
+
// .filters button:hover, the .steps/.humanSteps number chips), and each of
|
|
341
|
+
// those is measured by its own PAIRS row above.
|
|
342
|
+
const strays = [];
|
|
343
|
+
for (const [where, text] of [['cosmos.css', cssSrc], ['projects.html', pageSrc]]) {
|
|
344
|
+
for (const m of text.matchAll(/([^{}@]+)\{([^{}]*)\}/g)) {
|
|
345
|
+
if (!/\ba\b/.test(m[1])) continue;
|
|
346
|
+
if (/(?:^|[;{\s])color\s*:\s*var\(\s*--accent-deep\s*\)/.test(m[2])) {
|
|
347
|
+
strays.push(`${where}: ${m[1].trim().slice(0, 70)}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
assert.deepEqual(strays, [], `\n ${strays.join('\n ')}\n`);
|
|
352
|
+
});
|
|
353
|
+
|
|
230
354
|
// The gate's LOCKED sky palette — deliberately absent from COLOR_VARS, so a
|
|
231
355
|
// branding pack can never re-theme these. Dark GROUNDS are allowed (dark mode IS
|
|
232
356
|
// the gate's space, by design); text painted with one shows Cloud Bongos orange
|
|
@@ -176,6 +176,81 @@ test('the done panel lights Live on a verified answer, never on status alone (ta
|
|
|
176
176
|
assert.doesNotMatch(fn, /Your project is up/, 'the status-alone headline is dead');
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
+
/* ── The done panel's GitHub step (task 1002855) ─────────────────────────────
|
|
180
|
+
'We created <repo> on GitHub for you' named a repo and handed the owner nothing
|
|
181
|
+
to click, while the manual-create branch right below it rendered a full button:
|
|
182
|
+
the one path where the PLATFORM did the work was the one that stranded them
|
|
183
|
+
(owner-reported during the task 1002697 walk). Adopt named a repo unlinked too. ── */
|
|
184
|
+
|
|
185
|
+
test('the auto-created repo is linked in the done panel, not merely named', async () => {
|
|
186
|
+
const { body } = await request('/projects', 'game.test.local');
|
|
187
|
+
const fn = (body.match(/function renderStepGithubRepo\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
188
|
+
assert.ok(fn, 'renderStepGithubRepo() must be extractable');
|
|
189
|
+
const auto = (fn.match(/if \(state\.repoAutoCreated\) \{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
190
|
+
assert.ok(auto, 'the auto-created branch must be extractable');
|
|
191
|
+
assert.match(auto, /We created/, 'this is the copy the owner reported');
|
|
192
|
+
// BOTH copies — scaffold landed and not yet landed — carry the link
|
|
193
|
+
assert.equal((auto.match(/repoLink/g) || []).length, 2,
|
|
194
|
+
'the landed and pre-landed copies must both link the repo');
|
|
195
|
+
assert.doesNotMatch(auto, /repoLabel/,
|
|
196
|
+
'no auto-created copy may still name the repo without linking it');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('the adopt branch links the repo it names', async () => {
|
|
200
|
+
const { body } = await request('/projects', 'game.test.local');
|
|
201
|
+
const fn = (body.match(/function renderStepGithubRepo\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
202
|
+
const adopt = (fn.match(/if \(state\.mode === 'adopt'\) \{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
203
|
+
assert.ok(adopt, 'the adopt branch must be extractable');
|
|
204
|
+
assert.equal((adopt.match(/repoLink/g) || []).length, 2,
|
|
205
|
+
'both adopt copies must link the repo the owner picked');
|
|
206
|
+
assert.doesNotMatch(adopt, /repoLabel/, 'adopt may not name a repo unlinked either');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('the auto-created link is a plain inline link, never the .ghbtn call to action', async () => {
|
|
210
|
+
const { body } = await request('/projects', 'game.test.local');
|
|
211
|
+
const fn = (body.match(/function renderStepGithubRepo\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
212
|
+
const decl = (fn.match(/var repoLink = [^\n]*/) || [''])[0];
|
|
213
|
+
assert.match(decl, /<a href="/, 'the link is built as an anchor');
|
|
214
|
+
assert.match(decl, /esc\(repoUrl\)/, 'the href goes through the page escaper');
|
|
215
|
+
assert.match(decl, /rel="noopener"/, 'a target=_blank link carries rel=noopener');
|
|
216
|
+
assert.doesNotMatch(decl, /ghbtn/, 'there is nothing to DO here — no button treatment');
|
|
217
|
+
// the manual-create branch keeps its real .ghbtn action, and an anchor nested
|
|
218
|
+
// inside that anchor would be malformed — so only the two done branches link
|
|
219
|
+
assert.match(fn, /class="ghbtn"/, 'the manual-create button survives untouched');
|
|
220
|
+
// and the plain link needs a style, or it falls back to the browser's blue
|
|
221
|
+
assert.match(body, /\.humanSteps p a\{/, 'the inline link is styled with the page palette');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('a draft with no stored repo URL renders no link — never a guessed one', async () => {
|
|
225
|
+
const { body } = await request('/projects', 'game.test.local');
|
|
226
|
+
const src = (body.match(/(function repoLinkFor\(\)\s*\{[\s\S]*?\n \})/) || [, ''])[1];
|
|
227
|
+
assert.ok(src, 'repoLinkFor() must be extractable');
|
|
228
|
+
const urlFor = (state) => new Function('state', `${src}\nreturn repoLinkFor();`)(state);
|
|
229
|
+
|
|
230
|
+
// the real auto-created path: GitHub's own html_url, kept on the draft
|
|
231
|
+
assert.equal(
|
|
232
|
+
urlFor({ mode: 'greenfield', owner: 'someone', repo: 'thing', repoAutoCreated: true, repoUrl: 'https://github.com/someone/thing' }),
|
|
233
|
+
'https://github.com/someone/thing');
|
|
234
|
+
// a draft written before task 1002855 has no stored URL — it degrades to none
|
|
235
|
+
assert.equal(
|
|
236
|
+
urlFor({ mode: 'greenfield', owner: 'someone', repo: 'thing', repoAutoCreated: true }), '');
|
|
237
|
+
// adopt has no server-supplied URL: the repo the owner picked IS the address
|
|
238
|
+
assert.equal(urlFor({ mode: 'adopt', owner: 'someone', repo: 'thing' }), 'https://github.com/someone/thing');
|
|
239
|
+
// nothing named yet, and anything that isn't a github.com URL, link to nothing
|
|
240
|
+
assert.equal(urlFor({ mode: 'adopt', owner: '', repo: '' }), '');
|
|
241
|
+
assert.equal(urlFor({ mode: 'greenfield', owner: 'a', repo: 'b', repoUrl: 'javascript:alert(1)' }), '');
|
|
242
|
+
assert.equal(urlFor({ mode: 'greenfield', owner: 'a', repo: 'b', repoUrl: 'https://evil.example/x' }), '');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('the create-repo response keeps the html_url it already returned', async () => {
|
|
246
|
+
const { body } = await request('/projects', 'game.test.local');
|
|
247
|
+
const fn = (body.match(/function ensureFreshRepo\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
248
|
+
assert.ok(fn, 'ensureFreshRepo() must be extractable');
|
|
249
|
+
assert.match(fn, /data\.repo && data\.repo\.html_url/, 'the URL is read off the response, not dropped');
|
|
250
|
+
assert.match(fn, /state\.repoUrl = made/, 'and stored on the draft so a resume keeps it');
|
|
251
|
+
assert.match(fn, /saveDraft\(\)/, 'the draft is persisted after the URL lands on state');
|
|
252
|
+
});
|
|
253
|
+
|
|
179
254
|
test('the address row surfaces the manual-DNS note the runner recorded (F16)', async () => {
|
|
180
255
|
const { body } = await request('/projects', 'game.test.local');
|
|
181
256
|
assert.match(body, /inst\.dns_note/, 'the card reads the dns_note the runner writes');
|