@bongos/core 1.19.125 → 1.19.127
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 +16 -16
- package/modules/builder-settings/builder-needs.js +59 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +20 -10
- package/public-landing/projects.html +17 -7
- package/src/bongos/db-kernel.js +15 -0
- package/src/bongos/db.js +2 -1
- package/src/bongos/routes/me.js +11 -1
- package/src/module-api.js +5 -1
- package/tests/builder_needs.mjs +114 -0
- package/tests/cosmos_token_contrast.mjs +90 -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.127",
|
|
6
|
+
"core_contract": "1.19.127",
|
|
7
|
+
"source_commit": "f4b67a76dc2ad911a7c9257ba4280de4ac7904c7",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-15T00:49:23.419Z",
|
|
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": 1569,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "deedfbf01298656cc039a9d567722f3689778d62d8aacc97a230ac4490a39c7a",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -2367,7 +2367,7 @@
|
|
|
2367
2367
|
{
|
|
2368
2368
|
"path": "modules/builder-settings/builder-needs.js",
|
|
2369
2369
|
"mode": "0000644",
|
|
2370
|
-
"sha256": "
|
|
2370
|
+
"sha256": "3cb7478d9e347147e331f0954a9178a1770918067a678efdb8caef96241dfb5a"
|
|
2371
2371
|
},
|
|
2372
2372
|
{
|
|
2373
2373
|
"path": "modules/builder-settings/db.js",
|
|
@@ -4167,12 +4167,12 @@
|
|
|
4167
4167
|
{
|
|
4168
4168
|
"path": "package-lock.json",
|
|
4169
4169
|
"mode": "0000644",
|
|
4170
|
-
"sha256": "
|
|
4170
|
+
"sha256": "ecb64df7be6949adcb0d88b5f21ca3014724b598fa1a6d939a3a50d8f9696aa6"
|
|
4171
4171
|
},
|
|
4172
4172
|
{
|
|
4173
4173
|
"path": "package.json",
|
|
4174
4174
|
"mode": "0000644",
|
|
4175
|
-
"sha256": "
|
|
4175
|
+
"sha256": "5be9320e605a4b67ba2d63fa4ab831b99bda91a847c11338fd5e0ed6a65d0779"
|
|
4176
4176
|
},
|
|
4177
4177
|
{
|
|
4178
4178
|
"path": "public-docs/index.html",
|
|
@@ -4412,7 +4412,7 @@
|
|
|
4412
4412
|
{
|
|
4413
4413
|
"path": "public-landing/assets/cosmos.css",
|
|
4414
4414
|
"mode": "0000644",
|
|
4415
|
-
"sha256": "
|
|
4415
|
+
"sha256": "a55b4dd516485060edb4f1cd0be5164bd2026d77186d9b19c1f728a64eda9c7b"
|
|
4416
4416
|
},
|
|
4417
4417
|
{
|
|
4418
4418
|
"path": "public-landing/buddha.png",
|
|
@@ -4427,7 +4427,7 @@
|
|
|
4427
4427
|
{
|
|
4428
4428
|
"path": "public-landing/projects.html",
|
|
4429
4429
|
"mode": "0000644",
|
|
4430
|
-
"sha256": "
|
|
4430
|
+
"sha256": "6dcb73054fa58d3b0254f1d82a7f639ed465db2c2fd1e83e26f29b80532047e5"
|
|
4431
4431
|
},
|
|
4432
4432
|
{
|
|
4433
4433
|
"path": "public-landing/world/assets/clouds/cloud-island-large.png",
|
|
@@ -5527,12 +5527,12 @@
|
|
|
5527
5527
|
{
|
|
5528
5528
|
"path": "src/bongos/db-kernel.js",
|
|
5529
5529
|
"mode": "0000644",
|
|
5530
|
-
"sha256": "
|
|
5530
|
+
"sha256": "f31dd847ed641bf49013c345b1f08f9e2794a875dad777de10daeda2a4ae1cf3"
|
|
5531
5531
|
},
|
|
5532
5532
|
{
|
|
5533
5533
|
"path": "src/bongos/db.js",
|
|
5534
5534
|
"mode": "0000644",
|
|
5535
|
-
"sha256": "
|
|
5535
|
+
"sha256": "d3dd606664855a94788751d13ab7fc62ccdc4a8df48f331ec97a5a81a8a6fa96"
|
|
5536
5536
|
},
|
|
5537
5537
|
{
|
|
5538
5538
|
"path": "src/bongos/github-repos.js",
|
|
@@ -5687,7 +5687,7 @@
|
|
|
5687
5687
|
{
|
|
5688
5688
|
"path": "src/bongos/routes/me.js",
|
|
5689
5689
|
"mode": "0000644",
|
|
5690
|
-
"sha256": "
|
|
5690
|
+
"sha256": "83657d8a2f8e1d81a80324350ddd983baf5346da3f88ce1f41c2211275f34440"
|
|
5691
5691
|
},
|
|
5692
5692
|
{
|
|
5693
5693
|
"path": "src/bongos/routes/modules.js",
|
|
@@ -5757,7 +5757,7 @@
|
|
|
5757
5757
|
{
|
|
5758
5758
|
"path": "src/module-api.js",
|
|
5759
5759
|
"mode": "0000644",
|
|
5760
|
-
"sha256": "
|
|
5760
|
+
"sha256": "80e087f2181512cfdf51030c27aeb7aaa4235421c4e06c9d7c4a000d418b0d0a"
|
|
5761
5761
|
},
|
|
5762
5762
|
{
|
|
5763
5763
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6147,7 +6147,7 @@
|
|
|
6147
6147
|
{
|
|
6148
6148
|
"path": "tests/builder_needs.mjs",
|
|
6149
6149
|
"mode": "0000644",
|
|
6150
|
-
"sha256": "
|
|
6150
|
+
"sha256": "49a8146cbb0ec5def5b48130ed61fdec6e9855f22fe5ad3a09b39ae2cb218ca8"
|
|
6151
6151
|
},
|
|
6152
6152
|
{
|
|
6153
6153
|
"path": "tests/bump_version.mjs",
|
|
@@ -6267,7 +6267,7 @@
|
|
|
6267
6267
|
{
|
|
6268
6268
|
"path": "tests/cosmos_token_contrast.mjs",
|
|
6269
6269
|
"mode": "0000644",
|
|
6270
|
-
"sha256": "
|
|
6270
|
+
"sha256": "3689ce2b23d0baedb5a277676e939af712cea8d0cb3c07857e33f36eb12e1233"
|
|
6271
6271
|
},
|
|
6272
6272
|
{
|
|
6273
6273
|
"path": "tests/cost.mjs",
|
|
@@ -90,13 +90,68 @@ function artKeyNeed(ctx) {
|
|
|
90
90
|
// pure (ctx) => need|null function here — every renderer picks it up for free.
|
|
91
91
|
const NEEDS = [artKeyNeed];
|
|
92
92
|
|
|
93
|
+
// ---- founding grace (task 1002739 / direction §5.6, idea 1000701) -----------
|
|
94
|
+
//
|
|
95
|
+
// A freshly-provisioned instance used to greet its founding owner with "your
|
|
96
|
+
// free art coverage has ended" — a demand as the payoff moment of the whole
|
|
97
|
+
// create flow. While the founding owner's first days are running, the roster
|
|
98
|
+
// leads with a welcome and every action_needed item is held at 'covered'
|
|
99
|
+
// (listed, honest, but not demanding). First-run is a time window, not a
|
|
100
|
+
// visit-counter: counting the first hall visit would end the grace on the very
|
|
101
|
+
// request that renders it.
|
|
102
|
+
//
|
|
103
|
+
// ctx.isFoundingOwner — is this builder the instance's first admitted builder?
|
|
104
|
+
// ctx.founderCreatedAt — that first builder's created_at (≈ the instance's
|
|
105
|
+
// birth on a newborn); the window anchors here.
|
|
106
|
+
// ctx.projectName — branding identity.productName, for the greeting.
|
|
107
|
+
// ctx.nowMs — injectable clock for tests; defaults to Date.now().
|
|
108
|
+
const FOUNDING_GRACE_DAYS = 7;
|
|
109
|
+
const FOUNDING_GRACE_MS = FOUNDING_GRACE_DAYS * 24 * 60 * 60 * 1000;
|
|
110
|
+
|
|
111
|
+
function foundingGraceActive(ctx = {}) {
|
|
112
|
+
if (!ctx.isFoundingOwner || !ctx.founderCreatedAt) return false;
|
|
113
|
+
const born = new Date(ctx.founderCreatedAt).getTime();
|
|
114
|
+
if (!Number.isFinite(born)) return false;
|
|
115
|
+
const now = typeof ctx.nowMs === 'number' ? ctx.nowMs : Date.now();
|
|
116
|
+
return now - born < FOUNDING_GRACE_MS;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function foundingWelcomeNeed(ctx) {
|
|
120
|
+
const name = ctx.projectName || 'your project';
|
|
121
|
+
return {
|
|
122
|
+
id: 'founding_welcome',
|
|
123
|
+
title: `Welcome to ${name}`,
|
|
124
|
+
what: 'Your project is standing, and this hall is where it gets built.',
|
|
125
|
+
learn_more_url: null,
|
|
126
|
+
state: 'covered',
|
|
127
|
+
severity: 'info',
|
|
128
|
+
message: `You made it — ${name} is up, and this hall is its workshop. ` +
|
|
129
|
+
'Look around at your own pace; if the platform ever needs something from you, ' +
|
|
130
|
+
'it will say so here once your first days are behind you.',
|
|
131
|
+
action: null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
93
135
|
// Compute the live list for a builder. `ctx` carries everything the need
|
|
94
136
|
// functions read (see each need's doc). Returns:
|
|
95
137
|
// { items: Need[], action_needed_count, has_action_needed }
|
|
96
138
|
// items is ordered: action_needed first (loudest), then covered, then satisfied,
|
|
97
|
-
// so any renderer that takes "the first one" gets the most urgent.
|
|
139
|
+
// so any renderer that takes "the first one" gets the most urgent. During the
|
|
140
|
+
// founding grace nothing is action_needed, so the welcome (prepended before the
|
|
141
|
+
// stable sort) is what that renderer shows.
|
|
98
142
|
function computeNeeds(ctx = {}) {
|
|
99
|
-
const
|
|
143
|
+
const grace = foundingGraceActive(ctx);
|
|
144
|
+
let items = NEEDS.map((fn) => fn(ctx)).filter(Boolean);
|
|
145
|
+
if (grace) {
|
|
146
|
+
items = items.map((n) => n.state !== 'action_needed' ? n : {
|
|
147
|
+
...n,
|
|
148
|
+
state: 'covered',
|
|
149
|
+
severity: 'info',
|
|
150
|
+
message: 'Nothing needed from you today — we’ll bring this up once your first days are behind you.',
|
|
151
|
+
action: null,
|
|
152
|
+
});
|
|
153
|
+
items.unshift(foundingWelcomeNeed(ctx));
|
|
154
|
+
}
|
|
100
155
|
const order = { action_needed: 0, covered: 1, satisfied: 2 };
|
|
101
156
|
items.sort((a, b) => (order[a.state] ?? 9) - (order[b.state] ?? 9));
|
|
102
157
|
const actionNeeded = items.filter((n) => n.state === 'action_needed');
|
|
@@ -111,5 +166,7 @@ module.exports = {
|
|
|
111
166
|
computeNeeds,
|
|
112
167
|
// exported for tests / direct reuse
|
|
113
168
|
artKeyNeed,
|
|
169
|
+
foundingGraceActive,
|
|
170
|
+
FOUNDING_GRACE_DAYS,
|
|
114
171
|
SETTINGS_PATH,
|
|
115
172
|
};
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.127",
|
|
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.127",
|
|
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.127",
|
|
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
|
+
--sea-ink:color-mix(in srgb, var(--sea) 65%, #ffffff);
|
|
101
102
|
--ok:#4ade80; --warn:#fbbf24; --err:#f87171;
|
|
102
103
|
--on-ok:#0b2e18;
|
|
103
104
|
--page-bg:#150c06;
|
|
@@ -124,6 +125,7 @@
|
|
|
124
125
|
--ink:#fdeedd; --ink-soft:#dcb693; --ink-faint:#b89272;
|
|
125
126
|
--rule:#5e3521; --rule-soft:#3f2317;
|
|
126
127
|
--accent-deep:color-mix(in srgb, var(--accent) 45%, #ffffff);
|
|
128
|
+
--sea-ink:color-mix(in srgb, var(--sea) 65%, #ffffff);
|
|
127
129
|
--ok:#4ade80; --warn:#fbbf24; --err:#f87171;
|
|
128
130
|
--on-ok:#0b2e18;
|
|
129
131
|
--page-bg:#150c06;
|
|
@@ -154,6 +156,10 @@
|
|
|
154
156
|
--err-soft:color-mix(in srgb, var(--err) 12%, var(--bg-card));
|
|
155
157
|
--accent-soft:color-mix(in srgb, var(--accent) 15%, var(--bg-card));
|
|
156
158
|
--sea-soft:color-mix(in srgb, var(--sea) 16%, var(--bg-card));
|
|
159
|
+
/* the strip's STATE-carrying marks (done fill, now ring/pulse) — light uses
|
|
160
|
+
--sea as-is; dark derives brighter (the --accent-deep pattern) so a pack's
|
|
161
|
+
light-tuned sea clears WCAG 1.4.11's 3:1 on the near-black card too */
|
|
162
|
+
--sea-ink:var(--sea);
|
|
157
163
|
--neutral-soft:color-mix(in srgb, var(--ink) 8%, var(--bg-card));
|
|
158
164
|
/* ink-derived so the shadow is a WARM brown on the marigold ground instead of
|
|
159
165
|
the cold navy it used to cast. The dark blocks override it with a plain black
|
|
@@ -225,7 +231,7 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
225
231
|
(.rec) — "live" always says when it was last verified. ── */
|
|
226
232
|
.chip{display:inline-flex;align-items:center;gap:7px;font-size:12px;font-weight:700;letter-spacing:0.3px;border-radius:999px;padding:4px 12px;background:var(--neutral-soft);color:var(--ink-soft);}
|
|
227
233
|
.chip .cdot{width:7px;height:7px;border-radius:50%;background:var(--ink-faint);flex:0 0 auto;}
|
|
228
|
-
.chip .rec{font-weight:600;
|
|
234
|
+
.chip .rec{font-weight:600;font-size:11.5px;}
|
|
229
235
|
.chip.busy{background:var(--accent-soft);color:var(--accent-deep);}
|
|
230
236
|
.chip.busy .cdot{background:var(--accent);animation:chipPulse 1.4s ease-in-out infinite;}
|
|
231
237
|
.chip.live{background:var(--ok-soft);color:var(--ok);}
|
|
@@ -297,13 +303,15 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
297
303
|
.strip .stages{display:flex;list-style:none;margin-bottom:6px;}
|
|
298
304
|
.strip .stages li{flex:1;display:flex;flex-direction:column;align-items:center;gap:7px;position:relative;}
|
|
299
305
|
.strip .stages li:not(:first-child)::before{content:"";position:absolute;top:11px;right:50%;width:100%;height:3px;border-radius:2px;background:var(--sea-soft);z-index:0;margin-right:12px;}
|
|
300
|
-
.strip .stages li.done:not(:first-child)::before{background:var(--sea);}
|
|
306
|
+
.strip .stages li.done:not(:first-child)::before{background:var(--sea-ink);}
|
|
301
307
|
.strip .sdot{position:relative;z-index:1;width:24px;height:24px;border-radius:50%;border:3px solid var(--sea-soft);background:var(--bg-card);display:flex;align-items:center;justify-content:center;}
|
|
302
|
-
.strip .stages li.done .sdot{border-color:var(--sea);background:var(--sea);}
|
|
303
|
-
|
|
304
|
-
.strip .stages li.
|
|
305
|
-
.strip .stages li.now .sdot
|
|
306
|
-
|
|
308
|
+
.strip .stages li.done .sdot{border-color:var(--sea-ink);background:var(--sea-ink);}
|
|
309
|
+
/* tick ink is --on-ok, not #fff — dark's bright fills took a white tick to 1.7:1 */
|
|
310
|
+
.strip .stages li.done .sdot::after{content:"";width:8px;height:5px;border-left:2.5px solid var(--on-ok);border-bottom:2.5px solid var(--on-ok);transform:rotate(-45deg) translate(1px,-1px);}
|
|
311
|
+
.strip .stages li.now .sdot{border-color:var(--sea-ink);}
|
|
312
|
+
.strip .stages li.now .sdot::after{content:"";width:8px;height:8px;border-radius:50%;background:var(--sea-ink);animation:beatPulse 1.2s ease-in-out infinite;}
|
|
313
|
+
/* scale-only: an opacity trough took the pulse below the 3:1 its token clears */
|
|
314
|
+
@keyframes beatPulse{0%,100%{transform:scale(.7)}50%{transform:scale(1.15)}}
|
|
307
315
|
.strip .slbl{font-size:12px;font-weight:700;color:var(--ink-faint);}
|
|
308
316
|
.strip .stages li.now .slbl,.strip .stages li.done .slbl{color:color-mix(in srgb,var(--sea) 40%,var(--ink));}
|
|
309
317
|
.strip .pnote{text-align:center;font-size:13px;font-weight:600;color:var(--ink-soft);min-height:18px;line-height:1.5;}
|
|
@@ -414,8 +422,8 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
414
422
|
/* Home LEAVES the hub, so it never takes the current-view pill and a hairline
|
|
415
423
|
sets it apart. Without it the header offered no way back to the landing page
|
|
416
424
|
except the wordmark, which nothing announced as a link. */
|
|
417
|
-
.navrow .navHome{color:var(--glass-ink);
|
|
418
|
-
.navrow .navHome:hover{
|
|
425
|
+
.navrow .navHome{color:var(--glass-ink);}
|
|
426
|
+
.navrow .navHome:hover{color:var(--chrome-accent);}
|
|
419
427
|
.navSep{width:1px;align-self:stretch;margin:4px 3px;background:var(--glass-border);flex:0 0 auto;}
|
|
420
428
|
/* below the mid breakpoint the three groups stack and centre rather than
|
|
421
429
|
fighting for one row — equal flanks only buy a centred nav while it fits */
|
|
@@ -440,7 +448,9 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
440
448
|
.search svg{width:15px;height:15px;fill:none;stroke:var(--glass-ink);stroke-width:2;flex:0 0 auto;}
|
|
441
449
|
.search input{flex:1;min-width:0;background:none;border:none;outline:none;font-family:var(--font-mono);
|
|
442
450
|
font-size:13px;color:var(--chrome-ink);}
|
|
443
|
-
.
|
|
451
|
+
/* no opacity dim — .75 washed this to 3.87:1, and the .mineBar override
|
|
452
|
+
inherited the multiply so its own token could never fix it (task 1002751) */
|
|
453
|
+
.search input::placeholder{color:var(--glass-ink);}
|
|
444
454
|
.search .count{font-family:var(--font-mono);font-size:11.5px;color:var(--glass-ink);white-space:nowrap;}
|
|
445
455
|
|
|
446
456
|
/* the only-mine filter — a real two-state control, not a link: it filters the
|
|
@@ -163,7 +163,12 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
163
163
|
step that can't be performed yet (its prerequisite — usually the domain — is missing) */
|
|
164
164
|
.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%);}
|
|
165
165
|
.humanSteps>li.sdone::before{content:"✓";background:var(--ok);color:var(--on-ok);}
|
|
166
|
-
|
|
166
|
+
/* locked = muted TOKENS, not subtree opacity — opacity multiplies after the
|
|
167
|
+
token resolves and took this copy below AA (task 1002751) */
|
|
168
|
+
.humanSteps>li.slocked{border-color:color-mix(in srgb,var(--line) 55%,var(--bg-card));}
|
|
169
|
+
.humanSteps>li.slocked b{color:var(--ink-soft);}
|
|
170
|
+
.humanSteps>li.slocked p{color:var(--ink-faint);}
|
|
171
|
+
.humanSteps>li.slocked::before{color:var(--ink-faint);}
|
|
167
172
|
.humanSteps details{margin-top:10px;}
|
|
168
173
|
.humanSteps summary{cursor:pointer;font-weight:700;font-size:12.5px;color:var(--ink-faint);}
|
|
169
174
|
.humanSteps details p{margin-top:8px;}
|
|
@@ -194,16 +199,19 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
194
199
|
content:"";position:absolute;top:11px;right:50%;width:100%;height:3px;border-radius:2px;
|
|
195
200
|
background:var(--sea-soft);z-index:0;margin-right:12px;
|
|
196
201
|
}
|
|
197
|
-
|
|
202
|
+
/* state marks paint --sea-ink / --on-ok, mirroring cosmos's .strip (task
|
|
203
|
+
1002751) — raw --sea sat at 3.03:1 in dark and the #fff tick at 1.74:1 on
|
|
204
|
+
dark's bright --ok */
|
|
205
|
+
.prog .stages li.done:not(:first-child)::before{background:var(--sea-ink);}
|
|
198
206
|
.prog .sdot{
|
|
199
207
|
position:relative;z-index:1;width:24px;height:24px;border-radius:50%;
|
|
200
208
|
border:3px solid var(--sea-soft);background:var(--bg-card);display:flex;align-items:center;justify-content:center;
|
|
201
209
|
}
|
|
202
|
-
.prog .stages li.done .sdot{border-color:var(--sea);background:var(--sea);}
|
|
203
|
-
.prog .stages li.done .sdot::after{content:"";width:8px;height:5px;border-left:2.5px solid
|
|
204
|
-
.prog .stages li.now .sdot{border-color:var(--sea);}
|
|
210
|
+
.prog .stages li.done .sdot{border-color:var(--sea-ink);background:var(--sea-ink);}
|
|
211
|
+
.prog .stages li.done .sdot::after{content:"";width:8px;height:5px;border-left:2.5px solid var(--on-ok);border-bottom:2.5px solid var(--on-ok);transform:rotate(-45deg) translate(1px,-1px);}
|
|
212
|
+
.prog .stages li.now .sdot{border-color:var(--sea-ink);}
|
|
205
213
|
.prog .stages li.now .sdot::after{
|
|
206
|
-
content:"";width:8px;height:8px;border-radius:50%;background:var(--sea);
|
|
214
|
+
content:"";width:8px;height:8px;border-radius:50%;background:var(--sea-ink);
|
|
207
215
|
animation:beatPulse 1.2s ease-in-out infinite;
|
|
208
216
|
}
|
|
209
217
|
/* idle labels match cosmos's .strip .slbl — the 55% sea mix washed to 4.15:1 in dark */
|
|
@@ -229,7 +237,9 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
229
237
|
|
|
230
238
|
/* the collapsed address step — a confirmation, not a form (task 1002704) */
|
|
231
239
|
.addrConfirm{border:2px solid color-mix(in srgb,var(--ok) 30%,var(--bg-card));border-radius:18px;padding:18px 20px;background:linear-gradient(180deg,color-mix(in srgb,var(--ok) 8%,var(--bg-card)) 0%,var(--bg-card) 100%);margin-bottom:16px;}
|
|
232
|
-
.addrConfirm.optedOut{border-color:var(--line);background:var(--bg);
|
|
240
|
+
.addrConfirm.optedOut{border-color:var(--line);background:var(--bg);}
|
|
241
|
+
.addrConfirm.optedOut .addrBig{color:var(--ink-soft);}
|
|
242
|
+
.addrConfirm.optedOut>p{color:var(--ink-faint);}
|
|
233
243
|
.addrBig{font-family:var(--font-display);font-weight:700;font-size:19px;letter-spacing:-0.3px;line-height:1.4;}
|
|
234
244
|
.addrBig code{font-family:var(--font-mono);font-size:15px;background:var(--bg-deep);border-radius:8px;padding:2px 9px;color:var(--accent-deep);word-break:break-all;}
|
|
235
245
|
.addrConfirm>p{font-size:13.5px;font-weight:500;color:var(--ink-soft);line-height:1.5;margin-top:6px;}
|
package/src/bongos/db-kernel.js
CHANGED
|
@@ -62,6 +62,20 @@ async function getBuilderById(id) {
|
|
|
62
62
|
return rows[0] ?? null;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// getFoundingBuilder — the instance's first admitted HUMAN builder (task
|
|
66
|
+
// 1002739): the founding owner a newborn instance greets, and the anchor of
|
|
67
|
+
// the builder-needs founding-grace window. system_role IS NULL is load-bearing:
|
|
68
|
+
// core migrations 065/067 seed the BFG + Discord-bot system principals into
|
|
69
|
+
// builders on EVERY instance before anyone can sign in, so the lowest ids on a
|
|
70
|
+
// newborn DB are not the founder. First human row by id, regardless of status —
|
|
71
|
+
// the founder stays the founder even if later deactivated.
|
|
72
|
+
async function getFoundingBuilder() {
|
|
73
|
+
const { rows } = await pool.query(
|
|
74
|
+
`SELECT id, created_at FROM builders WHERE system_role IS NULL ORDER BY id ASC LIMIT 1`
|
|
75
|
+
);
|
|
76
|
+
return rows[0] ?? null;
|
|
77
|
+
}
|
|
78
|
+
|
|
65
79
|
// listActiveBuilders — the minimal, any-builder-readable roster (task 1877): an
|
|
66
80
|
// invite-picker needs a real name of every builder to invite, not just the
|
|
67
81
|
// leaderboard's top-100-by-credits window. Deliberately narrow columns — no
|
|
@@ -337,6 +351,7 @@ module.exports = {
|
|
|
337
351
|
withTx,
|
|
338
352
|
sessionPermsSelect,
|
|
339
353
|
getBuilderById,
|
|
354
|
+
getFoundingBuilder,
|
|
340
355
|
listActiveBuilders,
|
|
341
356
|
insertAuditLog,
|
|
342
357
|
LIVE_RANK_LADDER,
|
package/src/bongos/db.js
CHANGED
|
@@ -38,7 +38,7 @@ const seams = require('../module-seams');
|
|
|
38
38
|
// published doorway resolves through. Re-exported below so every caller is
|
|
39
39
|
// byte-identical; db-kernel depends only on ./pool, never ./db (one-way).
|
|
40
40
|
const {
|
|
41
|
-
withTx, getBuilderById, listActiveBuilders, insertAuditLog, LIVE_RANK_LADDER,
|
|
41
|
+
withTx, getBuilderById, getFoundingBuilder, listActiveBuilders, insertAuditLog, LIVE_RANK_LADDER,
|
|
42
42
|
// Auth/identity/admission + the rank vocabulary carved to db-kernel (ADR 0102,
|
|
43
43
|
// finishing ADR 0091 §1). Re-exported below so db.js's other importers + its own
|
|
44
44
|
// internal uses (setBuilderRank's RANKS.indexOf, etc.) stay byte-identical.
|
|
@@ -978,6 +978,7 @@ module.exports = {
|
|
|
978
978
|
upsertBuilder,
|
|
979
979
|
seatFirstAdminIfUnclaimed,
|
|
980
980
|
getBuilderById,
|
|
981
|
+
getFoundingBuilder,
|
|
981
982
|
listActiveBuilders,
|
|
982
983
|
linkAnthropicAccount,
|
|
983
984
|
getBuilderByGithubId,
|
package/src/bongos/routes/me.js
CHANGED
|
@@ -83,7 +83,7 @@ module.exports = function buildMeRouter() {
|
|
|
83
83
|
const [
|
|
84
84
|
builder, activeClaims, estVsActual, unlocked, lockedWithProgress, streakInfo,
|
|
85
85
|
recentGrades, builderAvg, onboardingRow, shipCount, recentKarma, karma30d, discordLink, boxConnected, cliAuthed,
|
|
86
|
-
behaviorPrefs, geminiKeyRow, needsRebaseTasks,
|
|
86
|
+
behaviorPrefs, geminiKeyRow, needsRebaseTasks, foundingBuilder,
|
|
87
87
|
] = await Promise.all([
|
|
88
88
|
db.getBuilderById(req.builder.id),
|
|
89
89
|
lc().getActiveClaimsForBuilder(req.builder.id),
|
|
@@ -144,6 +144,10 @@ module.exports = function buildMeRouter() {
|
|
|
144
144
|
// without a follow-up call. Through the `lifecycle` port; [] on a
|
|
145
145
|
// lifecycle-less instance (guarded — the port is resolveOptional).
|
|
146
146
|
lc() ? lc().needsRebaseTasksForBuilder(req.builder.id) : [],
|
|
147
|
+
// task 1002739: the instance's first admitted builder — anchors the
|
|
148
|
+
// builder-needs founding-grace window (a newborn instance's founder gets a
|
|
149
|
+
// welcome, not a nag). Best-effort: null just means no grace.
|
|
150
|
+
db.getFoundingBuilder().catch(() => null),
|
|
147
151
|
]);
|
|
148
152
|
// V3.R84 (#302), criterion C3: the builder's month-to-date spend vs their
|
|
149
153
|
// monthly budget. Surfaced here so the CLI (session-start ping) and the hall
|
|
@@ -274,6 +278,12 @@ module.exports = function buildMeRouter() {
|
|
|
274
278
|
rank: builder?.rank,
|
|
275
279
|
ownKeySet: !!(geminiKeyRow && geminiKeyRow.enc),
|
|
276
280
|
sharedKeyConfigured: artSharedKeyConfigured(),
|
|
281
|
+
// founding grace (task 1002739 / direction §5.6): a newborn instance's
|
|
282
|
+
// founder gets a welcome roster, not a coverage nag. The module owns the
|
|
283
|
+
// policy (window length, copy); we pass the raw facts.
|
|
284
|
+
isFoundingOwner: !!(foundingBuilder && String(foundingBuilder.id) === String(req.builder.id)),
|
|
285
|
+
founderCreatedAt: foundingBuilder ? foundingBuilder.created_at : null,
|
|
286
|
+
projectName: (() => { try { return branding().identity?.productName || null; } catch { return null; } })(),
|
|
277
287
|
}) : [],
|
|
278
288
|
};
|
|
279
289
|
// Module→core contribution seam (BV1.R42, ADR 0083). Let each enabled module
|
package/src/module-api.js
CHANGED
|
@@ -677,8 +677,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
677
677
|
// landed since 1.19.123 with no explicit bump. run 31844281033. (task 1002620)
|
|
678
678
|
// 1.19.125 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
679
679
|
// landed since 1.19.124 with no explicit bump. run 31849445746. (task 1002620)
|
|
680
|
+
// 1.19.126 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
681
|
+
// landed since 1.19.125 with no explicit bump. run 31853055269. (task 1002620)
|
|
682
|
+
// 1.19.127 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
683
|
+
// landed since 1.19.126 with no explicit bump. run 31854730552. (task 1002620)
|
|
680
684
|
// ---------------------------------------------------------------------------
|
|
681
|
-
const CORE_VERSION = '1.19.
|
|
685
|
+
const CORE_VERSION = '1.19.127'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
682
686
|
|
|
683
687
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
684
688
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/builder_needs.mjs
CHANGED
|
@@ -95,5 +95,119 @@ t('missing rank fails safe — not action_needed without an explicit rank', () =
|
|
|
95
95
|
assert.ok(['covered', 'action_needed', 'satisfied'].includes(n.state));
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
+
console.log('founding grace (task 1002739, direction §5.6):');
|
|
99
|
+
|
|
100
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
101
|
+
const bornNow = new Date().toISOString();
|
|
102
|
+
|
|
103
|
+
t('newborn founder: welcome item first, greeting by project name', () => {
|
|
104
|
+
const r = needs.computeNeeds({
|
|
105
|
+
rank: 'archon', ownKeySet: false,
|
|
106
|
+
isFoundingOwner: true, founderCreatedAt: bornNow, projectName: 'Mercury',
|
|
107
|
+
});
|
|
108
|
+
assert.equal(r.items[0].id, 'founding_welcome');
|
|
109
|
+
assert.match(r.items[0].title, /Mercury/);
|
|
110
|
+
assert.equal(r.items[0].state, 'covered');
|
|
111
|
+
assert.equal(r.items[0].action, null);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
t('newborn founder: zero action_needed — every nag is suppressed', () => {
|
|
115
|
+
const r = needs.computeNeeds({
|
|
116
|
+
rank: 'archon', ownKeySet: false, isFoundingOwner: true, founderCreatedAt: bornNow,
|
|
117
|
+
});
|
|
118
|
+
assert.equal(r.has_action_needed, false);
|
|
119
|
+
assert.equal(r.action_needed_count, 0);
|
|
120
|
+
for (const n of r.items) assert.notEqual(n.state, 'action_needed');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
t('a suppressed nag keeps its identity but loses the demand', () => {
|
|
124
|
+
const r = needs.computeNeeds({
|
|
125
|
+
rank: 'archon', ownKeySet: false, isFoundingOwner: true, founderCreatedAt: bornNow,
|
|
126
|
+
});
|
|
127
|
+
const artItem = r.items.find((n) => n.id === 'art_key');
|
|
128
|
+
assert.ok(artItem, 'the art_key item must still be listed, not hidden');
|
|
129
|
+
assert.equal(artItem.state, 'covered');
|
|
130
|
+
assert.equal(artItem.action, null);
|
|
131
|
+
assert.doesNotMatch(artItem.message, /coverage has ended/i);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
t('the grace window is seven days — the policy number is pinned, not self-referential', () => {
|
|
135
|
+
// A fixed injected clock, and 7 written as a literal on both sides: mutating
|
|
136
|
+
// FOUNDING_GRACE_DAYS (a "tune", a bad merge) must fail here, not slide.
|
|
137
|
+
assert.equal(needs.FOUNDING_GRACE_DAYS, 7);
|
|
138
|
+
const born = Date.parse('2026-01-01T00:00:00Z');
|
|
139
|
+
const ctx = { rank: 'archon', ownKeySet: false, isFoundingOwner: true, founderCreatedAt: '2026-01-01T00:00:00Z' };
|
|
140
|
+
const inWindow = needs.computeNeeds({ ...ctx, nowMs: born + 7 * DAY - 60_000 });
|
|
141
|
+
assert.equal(inWindow.has_action_needed, false);
|
|
142
|
+
assert.equal(inWindow.items[0].id, 'founding_welcome');
|
|
143
|
+
const expired = needs.computeNeeds({ ...ctx, nowMs: born + 7 * DAY + 60_000 });
|
|
144
|
+
assert.equal(expired.has_action_needed, true);
|
|
145
|
+
assert.ok(!expired.items.find((n) => n.id === 'founding_welcome'));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
t('grace expires: past FOUNDING_GRACE_DAYS the founder is nagged like anyone (real clock)', () => {
|
|
149
|
+
const old = new Date(Date.now() - (needs.FOUNDING_GRACE_DAYS * DAY + 60_000)).toISOString();
|
|
150
|
+
const r = needs.computeNeeds({
|
|
151
|
+
rank: 'archon', ownKeySet: false, isFoundingOwner: true, founderCreatedAt: old,
|
|
152
|
+
});
|
|
153
|
+
assert.equal(r.has_action_needed, true);
|
|
154
|
+
assert.ok(!r.items.find((n) => n.id === 'founding_welcome'));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
t('a non-founder on a newborn instance is not suppressed', () => {
|
|
158
|
+
const r = needs.computeNeeds({
|
|
159
|
+
rank: 'thetes', ownKeySet: false, isFoundingOwner: false, founderCreatedAt: bornNow,
|
|
160
|
+
});
|
|
161
|
+
assert.equal(r.has_action_needed, true);
|
|
162
|
+
assert.ok(!r.items.find((n) => n.id === 'founding_welcome'));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
t('satisfied stays satisfied during grace — no fake downgrades', () => {
|
|
166
|
+
const r = needs.computeNeeds({
|
|
167
|
+
rank: 'archon', ownKeySet: true, isFoundingOwner: true, founderCreatedAt: bornNow,
|
|
168
|
+
});
|
|
169
|
+
assert.equal(r.items.find((n) => n.id === 'art_key').state, 'satisfied');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
t('no founder facts at all → no grace, no welcome (vanilla ctx unchanged)', () => {
|
|
173
|
+
const r = needs.computeNeeds({ rank: 'thetes', ownKeySet: false });
|
|
174
|
+
assert.equal(r.has_action_needed, true);
|
|
175
|
+
assert.ok(!r.items.find((n) => n.id === 'founding_welcome'));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
console.log('the producing side (DB-free source pins — the policy is only as real as its facts):');
|
|
179
|
+
|
|
180
|
+
// The seven policy cases above hand computeNeeds its ctx. These pin the two
|
|
181
|
+
// places that PRODUCE that ctx, because both were wrong-or-unverified once:
|
|
182
|
+
// the founder query returned the migration-seeded BFG system principal (ids 1-2
|
|
183
|
+
// on every newborn DB — review panel, 2026-08-15), and a key rename in me.js
|
|
184
|
+
// would silently disable the grace while every policy case stayed green. A
|
|
185
|
+
// DB-backed newborn-instance test can't run in the unit gate, so the source is
|
|
186
|
+
// the checkable artifact.
|
|
187
|
+
import { readFileSync } from 'node:fs';
|
|
188
|
+
import { fileURLToPath } from 'node:url';
|
|
189
|
+
import path from 'node:path';
|
|
190
|
+
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
191
|
+
|
|
192
|
+
t('getFoundingBuilder skips the seeded system principals (system_role IS NULL)', () => {
|
|
193
|
+
const src = readFileSync(path.join(ROOT, 'src/bongos/db-kernel.js'), 'utf8');
|
|
194
|
+
const fn = src.slice(src.indexOf('function getFoundingBuilder'));
|
|
195
|
+
const sql = fn.slice(0, fn.indexOf('return'));
|
|
196
|
+
assert.match(sql, /FROM builders/);
|
|
197
|
+
assert.match(sql, /system_role IS NULL/,
|
|
198
|
+
'the founder query must exclude system principals — migrations 065/067 seed them at the lowest ids on every instance');
|
|
199
|
+
assert.match(sql, /ORDER BY id ASC/);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
t('the /me aggregator feeds computeNeeds the founder facts under their contract names', () => {
|
|
203
|
+
const src = readFileSync(path.join(ROOT, 'src/bongos/routes/me.js'), 'utf8');
|
|
204
|
+
const call = src.slice(src.indexOf('computeNeeds({'), src.indexOf('}) : []'));
|
|
205
|
+
for (const key of ['isFoundingOwner', 'founderCreatedAt', 'projectName']) {
|
|
206
|
+
assert.match(call, new RegExp(`${key}\\s*:`),
|
|
207
|
+
`me.js must pass ${key} into computeNeeds — a rename silently disables the founding grace`);
|
|
208
|
+
}
|
|
209
|
+
assert.match(src, /getFoundingBuilder\(\)/, 'me.js must resolve the founder row');
|
|
210
|
+
});
|
|
211
|
+
|
|
98
212
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
99
213
|
process.exit(failed ? 1 : 0);
|
|
@@ -165,6 +165,21 @@ const PAIRS = [
|
|
|
165
165
|
// .chip.busy + the .tbl .pill / .cardRel / builder-badge family: accent-deep
|
|
166
166
|
// ink on the accent-soft tint, both modes
|
|
167
167
|
['.chip.busy label', 'var(--accent-deep)', 'var(--accent-soft)', 4.5],
|
|
168
|
+
// lifecycle strip STATE INDICATORS (task 1002751): non-text at the WCAG
|
|
169
|
+
// 1.4.11 bar of 3.0. --sea-ink derives brighter in dark exactly the way
|
|
170
|
+
// --accent-deep does, so a pack's light-tuned sea can't vanish on the dark
|
|
171
|
+
// card; the done tick takes --on-ok (the ink-on-a-filled-state-dot token).
|
|
172
|
+
['strip done/now indicator on card', 'var(--sea-ink)', CARD, 3.0],
|
|
173
|
+
['strip done fill vs its idle rail', 'var(--sea-ink)', 'var(--sea-soft)', 3.0],
|
|
174
|
+
['strip indicator on the strip tint', 'var(--sea-ink)', 'color-mix(in srgb,var(--sea) 8%,var(--bg-card))', 3.0],
|
|
175
|
+
['strip done tick on its fill', 'var(--on-ok)', 'var(--sea-ink)', 3.0],
|
|
176
|
+
['flowDown done tick on its fill', 'var(--on-ok)', 'var(--ink-faint)', 3.0],
|
|
177
|
+
['.steps done/now ring on card', 'var(--accent)', CARD, 3.0],
|
|
178
|
+
['.steps done vs idle connector', 'var(--accent)', 'var(--line)', 3.0],
|
|
179
|
+
// the opted-out address card swaps to the page ground (--bg); its muted copy
|
|
180
|
+
// must still measure (19px/700 headline = large text, 3.0)
|
|
181
|
+
['optedOut .addrBig (large) on --bg', 'var(--ink-soft)', 'var(--bg)', 3.0],
|
|
182
|
+
['addrOpt warning on --bg', 'var(--warn)', 'var(--bg)', 4.5],
|
|
168
183
|
// lifecycle strip: 12px/700
|
|
169
184
|
['.strip .slbl idle', 'var(--ink-faint)', CARD, 4.5],
|
|
170
185
|
['.strip .slbl now/done', 'color-mix(in srgb,var(--sea) 40%,var(--ink))', CARD, 4.5],
|
|
@@ -254,6 +269,81 @@ test('dark blocks never re-point a token at the locked space palette', () => {
|
|
|
254
269
|
}
|
|
255
270
|
});
|
|
256
271
|
|
|
272
|
+
// opacity multiplies AFTER the token resolves, so a dimmed subtree's contrast
|
|
273
|
+
// is invisible to every token assertion above (task 1002751: .slocked at .6
|
|
274
|
+
// took 13.5px copy to 3.02:1; .chip .rec at .72 took recency to 3.48:1;
|
|
275
|
+
// .addrConfirm.optedOut at .75 took its note to 3.43:1; a placeholder rule's
|
|
276
|
+
// .75 survived into an override that set only the color, so no token value
|
|
277
|
+
// could ever fix it). Dim with a muted TOKEN so the result stays measured.
|
|
278
|
+
// Known limits, accepted: @keyframes bodies, animation-fill-mode resting
|
|
279
|
+
// values, and JS-set inline opacity are out of static reach — never make an
|
|
280
|
+
// animation or script the resting presenter of text or a state mark.
|
|
281
|
+
const pageSrc = fs
|
|
282
|
+
.readFileSync(path.join(path.dirname(CSS), '..', 'projects.html'), 'utf8')
|
|
283
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
284
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
285
|
+
const cssSrc = src.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
286
|
+
const dimsBelowFull = (body) => {
|
|
287
|
+
const m = body.match(/opacity\s*:\s*([\d.]+%?)/);
|
|
288
|
+
if (!m) return false;
|
|
289
|
+
const v = m[1].endsWith('%') ? parseFloat(m[1]) / 100 : parseFloat(m[1]);
|
|
290
|
+
return v < 1;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
test('text is never dimmed by subtree opacity', () => {
|
|
294
|
+
// Two nets. (1) Anywhere in either sheet: a rule that declares BOTH a color
|
|
295
|
+
// and a sub-1 opacity is dimmed text by construction. (2) The create flow's
|
|
296
|
+
// known text containers may not carry opacity at all — this also catches
|
|
297
|
+
// ancestor dims (.createSplit) that net 1 can't see.
|
|
298
|
+
const offenders = [];
|
|
299
|
+
for (const [where, text] of [['cosmos.css', cssSrc], ['projects.html', pageSrc]]) {
|
|
300
|
+
for (const m of text.matchAll(/([^{}@]+)\{([^{}]*)\}/g)) {
|
|
301
|
+
if (/(?:^|[;{\s])color\s*:/.test(m[2]) && dimsBelowFull(m[2])) {
|
|
302
|
+
offenders.push(`${where}: ${m[1].trim().slice(0, 60)} declares color + opacity<1`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
for (const sel of ['\\.humanSteps', '\\.addrConfirm', '\\.prog\\b', '\\.choice', '\\.review', '\\.createSplit', '\\.chip \\.rec']) {
|
|
307
|
+
for (const text of [cssSrc, pageSrc]) {
|
|
308
|
+
for (const m of text.matchAll(new RegExp(`${sel}[^{}]*\\{([^}]*)\\}`, 'g'))) {
|
|
309
|
+
if (dimsBelowFull(m[1])) offenders.push(`text container dimmed: ${m[0].slice(0, 90)}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
assert.deepEqual(offenders, [], `\n ${offenders.join('\n ')}\n`);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('the .prog strip mirrors .strip: state marks are --sea-ink, ticks are --on-ok', () => {
|
|
317
|
+
// /create's done panel renders the page-local .prog, NOT cosmos's .strip —
|
|
318
|
+
// a re-point of one and not the other is exactly how the 1.74:1 dark tick
|
|
319
|
+
// shipped the first time. State-mark rules (done fill/connector, now
|
|
320
|
+
// ring/pulse, the tick) must use the derived tokens; raw --sea is only legal
|
|
321
|
+
// as a tint ingredient (color-mix) or the idle -soft rail.
|
|
322
|
+
const progRules = [...pageSrc.matchAll(/\.prog[^{}]*\{([^}]*)\}/g)];
|
|
323
|
+
assert.ok(progRules.length >= 5, 'the .prog rules moved out of projects.html — update this test');
|
|
324
|
+
const bad = [];
|
|
325
|
+
for (const m of progRules) {
|
|
326
|
+
const body = m[1];
|
|
327
|
+
if (/(?:border(?:-\w+)?|background)\s*:\s*var\(\s*--sea\s*\)/.test(body)) {
|
|
328
|
+
bad.push(`raw --sea state mark: ${m[0].slice(0, 80)}`);
|
|
329
|
+
}
|
|
330
|
+
if (/border-(?:left|bottom)\s*:[^;]*#fff\b/i.test(body)) {
|
|
331
|
+
bad.push(`hardcoded #fff tick: ${m[0].slice(0, 80)}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
assert.deepEqual(bad, [], `\n ${bad.join('\n ')}\n`);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('the pulse animation never dims its mark', () => {
|
|
338
|
+
// beatPulse drives the now-dot in BOTH .strip and .prog. An opacity trough
|
|
339
|
+
// (the old 0.6) put the pulsing mark below the 3:1 its token clears at rest
|
|
340
|
+
// — the PAIRS row above would assert a color the pixel never sustains.
|
|
341
|
+
const kf = cssSrc.match(/@keyframes beatPulse\{([\s\S]*?)\}\s*\}/) ||
|
|
342
|
+
cssSrc.match(/@keyframes beatPulse\{([^@]*)\}/);
|
|
343
|
+
assert.ok(kf, '@keyframes beatPulse not found in cosmos.css — update this test');
|
|
344
|
+
assert.ok(!/opacity\s*:/.test(kf[1]), 'beatPulse dims the now-mark by opacity again');
|
|
345
|
+
});
|
|
346
|
+
|
|
257
347
|
test('the two dark blocks stay in lockstep', () => {
|
|
258
348
|
// cosmos.css carries dark twice — an explicit data-mode hook and a
|
|
259
349
|
// prefers-color-scheme twin. A change to one and not the other is the
|