@bongos/core 1.19.93 → 1.19.94
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 +24 -89
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +240 -0
- package/public-landing/index.html +10 -4
- package/public-landing/projects.html +663 -44
- package/scripts/gds/run-unit-tests.js +0 -1
- package/src/bongos/module-scope-map.js +1 -1
- package/src/module-api.js +3 -1
- package/src/platform-server.js +24 -42
- package/tests/known_failing_ratchet.mjs +7 -8
- package/tests/landing_page.mjs +15 -2
- package/tests/projects_hub.mjs +205 -0
- package/public-landing/gate/README.md +0 -137
- package/public-landing/gate/dispmaps/bar.png +0 -0
- package/public-landing/gate/dispmaps/flat.png +0 -0
- package/public-landing/gate/dispmaps/key.png +0 -0
- package/public-landing/gate/index.html +0 -634
- package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
- package/public-landing/gate/previews/off-the-boats.jpg +0 -0
- package/public-landing/gate/space.css +0 -809
- package/public-landing/gate/space.js +0 -1848
- package/public-landing/gate/sprites/enter.png +0 -0
- package/public-landing/gate/sprites/search.png +0 -0
- package/public-landing/gate/sprites/terminal-blank.png +0 -0
- package/public-landing/gate/sprites/tile.png +0 -0
- package/tests/gate_launch_metadata.mjs +0 -67
|
@@ -102,7 +102,6 @@ const KNOWN_FAILING = new Set([
|
|
|
102
102
|
// diagnoses live in that task. Remove each here as it is fixed.
|
|
103
103
|
// PAIRING: tests/known_failing_ratchet.mjs keeps EXECUTING each parked test
|
|
104
104
|
// and fails the suite the moment one starts passing — so this set cannot rot.
|
|
105
|
-
'platform_boot',
|
|
106
105
|
'served_paths_shipped',
|
|
107
106
|
]);
|
|
108
107
|
|
|
@@ -192,7 +192,7 @@ const MODULE_GLOBS = {
|
|
|
192
192
|
// (moduleWebSurfaces → serve-internal). Its public read endpoints stay SHARED
|
|
193
193
|
// core in routes/public.js (the hall + the off-box mirror consume them too).
|
|
194
194
|
'status-ui': ['modules/status-ui/'],
|
|
195
|
-
// The instance's public landing surface (index/
|
|
195
|
+
// The instance's public landing surface (index/projects/create), served by
|
|
196
196
|
// platform-server.js from the repo-root public-landing/ dir — not yet a carved
|
|
197
197
|
// module, so the glob lists where the code lives TODAY (the auto-tighten
|
|
198
198
|
// contract above). Its own key, not provisioning's: the front door is broader
|
package/src/module-api.js
CHANGED
|
@@ -613,8 +613,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
613
613
|
// landed since 1.19.91 with no explicit bump. run 31727411036. (task 1002620)
|
|
614
614
|
// 1.19.93 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
615
615
|
// landed since 1.19.92 with no explicit bump. run 31735192921. (task 1002620)
|
|
616
|
+
// 1.19.94 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
617
|
+
// landed since 1.19.93 with no explicit bump. run 31742308532. (task 1002620)
|
|
616
618
|
// ---------------------------------------------------------------------------
|
|
617
|
-
const CORE_VERSION = '1.19.
|
|
619
|
+
const CORE_VERSION = '1.19.94'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
618
620
|
|
|
619
621
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
620
622
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/src/platform-server.js
CHANGED
|
@@ -96,19 +96,6 @@ function stripApexOnly(html, isHub = isModuleEnabled('platform-identity')) {
|
|
|
96
96
|
return isHub ? html : String(html).replace(APEX_ONLY, '');
|
|
97
97
|
}
|
|
98
98
|
function landingHtml() { return landingPage('index.html'); }
|
|
99
|
-
// The interactive landing gate (public-landing/gate/, ported from amazonprimea
|
|
100
|
-
// in task 1002407). When an instance ships it, it is the apex front door
|
|
101
|
-
// (task 1002408). Unlike the placeholder landing it is a self-contained baked
|
|
102
|
-
// design that reads no window.__BRANDING__, so it is served RAW, not injected.
|
|
103
|
-
// Memoized: absolute path to gate/index.html, or '' when the instance ships no gate.
|
|
104
|
-
let _gateIndexPath; // undefined = unresolved, '' = absent, string = present
|
|
105
|
-
function gateIndexPath() {
|
|
106
|
-
if (_gateIndexPath === undefined) {
|
|
107
|
-
const p = path.join(LANDING_DIR, 'gate', 'index.html');
|
|
108
|
-
_gateIndexPath = fs.existsSync(p) ? p : '';
|
|
109
|
-
}
|
|
110
|
-
return _gateIndexPath;
|
|
111
|
-
}
|
|
112
99
|
function isApexHost(req) {
|
|
113
100
|
return !isStatusHost(req) && !isBuildersHost(req);
|
|
114
101
|
}
|
|
@@ -127,38 +114,30 @@ function buildPlatformApp() {
|
|
|
127
114
|
// The whole internal surface — shared verbatim with server.js (ADR 0062 R51).
|
|
128
115
|
mountInternalSurfaces(app);
|
|
129
116
|
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
117
|
+
// THE APEX FRONT DOOR (task 1002743). GET / serves the real landing page,
|
|
118
|
+
// branding-injected and apex-stripped like every other landing surface.
|
|
119
|
+
//
|
|
120
|
+
// It used to 302 to /gate/ — the interactive orb sky (task 1002408). That
|
|
121
|
+
// redirect is gone with the gate itself: the landing page is now the front
|
|
122
|
+
// door and /projects is the hub the sky became a view inside. The redirect
|
|
123
|
+
// could only be removed once the landing page carried real doors into the
|
|
124
|
+
// hub, which it does (front-door-and-hub-direction.md §3); removing it
|
|
125
|
+
// earlier would have left navigation worse than it was.
|
|
126
|
+
//
|
|
127
|
+
// NOTE THE INFRA HALF: on a live instance the reverse proxy may carry its own
|
|
128
|
+
// `redir / /gate/ 302` (ADR 0152 records the Caddy @gate block). Caddy runs
|
|
129
|
+
// that before the request ever reaches Node, so this change alone does not
|
|
130
|
+
// move a deployed apex — the operator has to drop that rule, and the
|
|
131
|
+
// pre-launch basic_auth it sits with, at launch.
|
|
132
|
+
//
|
|
133
|
+
// Falls through to the 404 tail when the instance ships no landing page, and
|
|
134
|
+
// to the landing asset handler below for non-/ paths (e.g. the mascot image).
|
|
139
135
|
app.get('/', (req, res, next) => {
|
|
140
136
|
if (!isApexHost(req)) return next();
|
|
141
|
-
if (gateIndexPath()) return res.redirect(302, '/gate/');
|
|
142
137
|
const html = landingHtml();
|
|
143
138
|
if (!html) return next();
|
|
144
139
|
res.type('html').send(html);
|
|
145
140
|
});
|
|
146
|
-
// /gate → /gate/ (so the gate's relative asset paths resolve against the dir),
|
|
147
|
-
// and /gate/ serves the gate's own index RAW — a self-contained baked design,
|
|
148
|
-
// so no window.__BRANDING__ injection. The gate's sub-assets (/gate/*, /world/*,
|
|
149
|
-
// /assets/*) are served by the express.static(LANDING_DIR) handler below with no
|
|
150
|
-
// extra rule, which also closes the amazonprimea Caddy gap where /assets/anim/idle/*
|
|
151
|
-
// (the buddha idle frames) fell through to the app instead of the static server.
|
|
152
|
-
// One handler for both forms — Express's non-strict routing matches /gate and
|
|
153
|
-
// /gate/ interchangeably, so branch on the real req.path: /gate 301→ /gate/
|
|
154
|
-
// (so relative asset paths resolve against the dir), /gate/ serves the index.
|
|
155
|
-
app.get(['/gate', '/gate/'], (req, res, next) => {
|
|
156
|
-
if (!isApexHost(req)) return next();
|
|
157
|
-
const p = gateIndexPath();
|
|
158
|
-
if (!p) return next();
|
|
159
|
-
if (!req.path.endsWith('/')) return res.redirect(301, '/gate/');
|
|
160
|
-
return res.type('html').sendFile(p);
|
|
161
|
-
});
|
|
162
141
|
// The branded project-creation experience (goal 35, task 2081) — a sibling
|
|
163
142
|
// landing page at a clean /create URL, branding-injected like the landing.
|
|
164
143
|
app.get('/create', (req, res, next) => {
|
|
@@ -167,9 +146,12 @@ function buildPlatformApp() {
|
|
|
167
146
|
if (!html) return next();
|
|
168
147
|
res.type('html').send(html);
|
|
169
148
|
});
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
149
|
+
// THE PROJECT HUB (task 1002743) — one surface holding every project-shaped
|
|
150
|
+
// action: the map of what exists (the orb sky the gate used to own, plus the
|
|
151
|
+
// directory table), the owner's own projects, and starting a new one. The
|
|
152
|
+
// view is chosen client-side from `?view=map|mine|new`; this route matches on
|
|
153
|
+
// path only and serves byte-identical HTML for every view, which is why all
|
|
154
|
+
// three ship as real markup rather than being built by script.
|
|
173
155
|
app.get('/projects', (req, res, next) => {
|
|
174
156
|
if (!isApexHost(req)) return next();
|
|
175
157
|
const html = landingPage('projects.html');
|
|
@@ -32,16 +32,15 @@ const TESTS_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
|
32
32
|
// ">=7 host tests" non-vacuity floor had gone stale after ADR 0107 moved the
|
|
33
33
|
// host-module tests to modules/<key>/tests/; it is a regex specimen self-test
|
|
34
34
|
// now, and idea 1000542 — which owned it — is superseded.)
|
|
35
|
-
|
|
35
|
+
// Ratcheted down 2 → 1 by task 1002743, which unparked platform_boot. The
|
|
36
|
+
// diagnosis its entry asked for turned out to be the one it suspected: the apex
|
|
37
|
+
// really was redirecting instead of serving the landing page, and the test was
|
|
38
|
+
// right all along. The apex flip removed the redirect, so the test passes and
|
|
39
|
+
// the entry had to go in the same change — this file fails the suite the moment
|
|
40
|
+
// a parked test starts passing.
|
|
41
|
+
const MAX_KNOWN_FAILING = 1;
|
|
36
42
|
|
|
37
43
|
const KNOWN_FAILING = {
|
|
38
|
-
platform_boot: {
|
|
39
|
-
owner: 'task 1002617 (parked in run-unit-tests KNOWN_FAILING by task 1002611)',
|
|
40
|
-
why: 'GET / on the apex returns 302, not the 200 landing page the test expects. '
|
|
41
|
-
+ 'Possibly a REAL product bug (the apex redirecting instead of serving the '
|
|
42
|
-
+ 'landing) rather than a test problem, so it was deliberately not "fixed" by '
|
|
43
|
-
+ 'relaxing the assertion. Diagnose the redirect first.',
|
|
44
|
-
},
|
|
45
44
|
served_paths_shipped: {
|
|
46
45
|
owner: 'task 1002617; collides with the ACTIVE claim "make the /docs/openapi.json endpoint self-healing"',
|
|
47
46
|
why: 'Asserts serve-internal.js reads docs/api/openapi.json. That exact path is what '
|
package/tests/landing_page.mjs
CHANGED
|
@@ -209,12 +209,25 @@ test('the ripples are hidden under reduced motion — they rest at full stroke',
|
|
|
209
209
|
});
|
|
210
210
|
|
|
211
211
|
test('the self-typing eyebrow degrades to real text, not an empty span', () => {
|
|
212
|
-
// the script only decorates; markup must already carry the
|
|
213
|
-
assert.match(HTML,
|
|
212
|
+
// the script only decorates; markup must already carry the whole sentence
|
|
213
|
+
assert.match(HTML, /type <b>bongos<\/b> in <span class="typed" id="typed">any AI<\/span>/);
|
|
214
214
|
assert.match(CODE, /matchMedia\('\(prefers-reduced-motion: reduce\)'\)/);
|
|
215
215
|
assert.match(CODE, /if \(!reduce\) \{/, 'the typing loop is skipped under reduced motion');
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
+
test('the eyebrow types the ASSISTANT, never the word you type (task 1002743)', () => {
|
|
219
|
+
// "bongos" is the constant — it is the word a person types — and the
|
|
220
|
+
// assistant is what varies. Cycling the first slot produced "type claude in
|
|
221
|
+
// any AI", which nobody does and which inverts the claim the line makes.
|
|
222
|
+
assert.doesNotMatch(HTML, /type <span class="typed"/, 'the typed span must not hold the typed-word slot');
|
|
223
|
+
const words = CODE.match(/words\s*=\s*\[([^\]]*)\]/);
|
|
224
|
+
assert.ok(words, 'the eyebrow word list must be findable');
|
|
225
|
+
assert.doesNotMatch(words[1], /'bongos'/, 'bongos is fixed copy, never a cycled word');
|
|
226
|
+
for (const ai of ['Claude', 'ChatGPT', 'Gemini']) {
|
|
227
|
+
assert.match(words[1], new RegExp(`'${ai}'`), `${ai} must be one of the cycled assistants`);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
218
231
|
test('nothing about the platform is hardcoded — the file is portable (C5)', () => {
|
|
219
232
|
// the source repo link is a real, deliberate attribution; every OTHER mention
|
|
220
233
|
// of an origin must be a token.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// tests/projects_hub.mjs — the /projects HUB contract (task 1002743).
|
|
2
|
+
//
|
|
3
|
+
// /projects is one surface holding three views — map (sky | table), your
|
|
4
|
+
// projects, and new project — under a persistent shell, selected by
|
|
5
|
+
// `?view=map|mine|new`. The design contract is docs/design/projects-hub-direction.md
|
|
6
|
+
// plus its approved mock; this file pins the parts of it a later change could
|
|
7
|
+
// break silently, and the numbered constraints it cites are that document's §7.
|
|
8
|
+
//
|
|
9
|
+
// Static analysis on purpose: the page's structure must be true of the FILE, not
|
|
10
|
+
// only of a booted server, because C7 is precisely "real markup, not JS-built".
|
|
11
|
+
// The served shape (branding injection, apex stripping, the id="projectsList"
|
|
12
|
+
// contract) is covered by tests/public_landing_projects.mjs.
|
|
13
|
+
|
|
14
|
+
import test from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
const HUB = fs.readFileSync(path.join(ROOT, 'public-landing', 'projects.html'), 'utf8');
|
|
22
|
+
const COSMOS = fs.readFileSync(path.join(ROOT, 'public-landing', 'assets', 'cosmos.css'), 'utf8');
|
|
23
|
+
const STYLE = (HUB.match(/<style>([\s\S]*?)<\/style>/) || [, ''])[1];
|
|
24
|
+
const SCRIPT = (HUB.match(/<script>([\s\S]*?)<\/script>\s*<\/body>/) || [, ''])[1];
|
|
25
|
+
|
|
26
|
+
test('all three views ship as real markup, not built by script (C7)', () => {
|
|
27
|
+
for (const v of ['map', 'mine', 'new']) {
|
|
28
|
+
assert.match(HUB, new RegExp(`id="view-${v}"`), `view-${v} must exist in the markup`);
|
|
29
|
+
}
|
|
30
|
+
// the map's two sub-views and the manage list are markup too
|
|
31
|
+
assert.match(HUB, /id="sub-sky-body"/);
|
|
32
|
+
assert.match(HUB, /id="sub-table-body"/);
|
|
33
|
+
assert.match(HUB, /id="projectsList"/);
|
|
34
|
+
// and the copy a crawler reads is in the document, not in a template string
|
|
35
|
+
assert.match(HUB, /Start a project/);
|
|
36
|
+
assert.match(HUB, /Your projects/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('the nav carries Home plus the three views, and Home is not a view', () => {
|
|
40
|
+
assert.match(HUB, /data-nav="home"[^>]*>Home|<a href="\/" class="navHome" data-nav="home">Home/);
|
|
41
|
+
for (const v of ['map', 'mine', 'new']) {
|
|
42
|
+
assert.match(HUB, new RegExp(`href="\\?view=${v}"\\s+data-nav="${v}"`),
|
|
43
|
+
`the nav must deep-link ?view=${v}`);
|
|
44
|
+
}
|
|
45
|
+
// Home navigates to the landing page; it must never take the current-view pill
|
|
46
|
+
assert.match(SCRIPT, /if \(a\.dataset\.nav === 'home'\) return;/,
|
|
47
|
+
'the view router must leave Home as a real navigation');
|
|
48
|
+
assert.match(SCRIPT, /a\.dataset\.nav !== 'home' && a\.dataset\.nav === v/,
|
|
49
|
+
'Home must be excluded from the aria-current pill');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('?view= is the deep-link contract, read on boot and written with replaceState', () => {
|
|
53
|
+
assert.match(SCRIPT, /new URLSearchParams\(location\.search\)\.get\('view'\)/);
|
|
54
|
+
assert.match(SCRIPT, /history\.replaceState\(null, '', '\?view=' \+ v\)/);
|
|
55
|
+
// the landing page's CTAs have always pointed here — this is the bug they hit
|
|
56
|
+
const landing = fs.readFileSync(path.join(ROOT, 'public-landing', 'index.html'), 'utf8');
|
|
57
|
+
assert.match(landing, /href="\/projects\?view=new"/,
|
|
58
|
+
'the landing CTA target must stay a view the hub knows');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('no page-local :root — it would silently defeat re-branding (C1)', () => {
|
|
62
|
+
// injectBrandingGlobal() splices the pack's :root at the END of <head>, at
|
|
63
|
+
// equal specificity but later in order. A page-local one outranks it forever.
|
|
64
|
+
assert.doesNotMatch(STYLE, /(^|\s):root\s*\{/,
|
|
65
|
+
'page-local :root defeats the branding pack — scope custom properties to their component');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('the page grounds on .sky, never on body (C3)', () => {
|
|
69
|
+
// themeMarkup() emits body{background:<light>} after cosmos.css in BOTH modes
|
|
70
|
+
// and is not mode-aware, so .sky is the only mode-aware full-page paint.
|
|
71
|
+
assert.match(HUB, /<div class="sky"/);
|
|
72
|
+
assert.match(COSMOS, /\.sky\{[^}]*background:var\(--sky-bg\)/);
|
|
73
|
+
assert.doesNotMatch(STYLE, /body\s*\{[^}]*background/,
|
|
74
|
+
'the hub must not paint its own body background');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('every dark override in the page is written twice (C2)', () => {
|
|
78
|
+
const explicit = (STYLE.match(/:root\[data-mode="dark"\]/g) || []).length;
|
|
79
|
+
const media = (STYLE.match(/:root:not\(\[data-mode="light"\]\)/g) || []).length;
|
|
80
|
+
assert.equal(explicit, media,
|
|
81
|
+
'each :root[data-mode="dark"] rule needs its prefers-color-scheme twin');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('apex-only sections use balanced sentinels so a child instance loses them (C5)', () => {
|
|
85
|
+
const starts = (HUB.match(/<!--apex-only:start-->/g) || []).length;
|
|
86
|
+
const ends = (HUB.match(/<!--apex-only:end-->/g) || []).length;
|
|
87
|
+
assert.equal(starts, ends, 'apex-only sentinels must be balanced');
|
|
88
|
+
assert.ok(starts >= 2, 'the hall link and the commons claim are both apex-only');
|
|
89
|
+
// the platform's own orb is apex-only, and it must be MARKUP — stripApexOnly()
|
|
90
|
+
// is a string transform and cannot reach anything a script renders later
|
|
91
|
+
const apexBlocks = HUB.match(/<!--apex-only:start-->[\s\S]*?<!--apex-only:end-->/g) || [];
|
|
92
|
+
assert.ok(apexBlocks.some((b) => b.includes('selfOrb')),
|
|
93
|
+
'the platform self-orb must sit inside the sentinels, in markup');
|
|
94
|
+
assert.ok(apexBlocks.some((b) => b.includes('buildersOrigin')),
|
|
95
|
+
'the hall link is apex-only — a child instance has no hall');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('cross-host links are server-stamped tokens, never client-filled', () => {
|
|
99
|
+
// clientBranding() deliberately withholds domains, so window.__BRANDING__ can
|
|
100
|
+
// never re-point a hall/apex href. Only the {{token}} pass can.
|
|
101
|
+
assert.match(HUB, /href="\{\{buildersOrigin\}\}"/);
|
|
102
|
+
assert.match(HUB, /<title>\{\{productName\}\}/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('the hall link is not labelled "Enter the Hall"', () => {
|
|
106
|
+
// tests/public_landing_projects.mjs forbids that copy on this page: the hall
|
|
107
|
+
// my-projects card was removed in task 1002675 and an owner must not be sent
|
|
108
|
+
// there to find their own projects.
|
|
109
|
+
assert.doesNotMatch(HUB, /Enter the Hall/i);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('orb accents are tokens, never literal hexes (§6)', () => {
|
|
113
|
+
// ONE palette serves both the map's spheres and the manage cards, so a
|
|
114
|
+
// project reads the same colour in either view
|
|
115
|
+
const accents = (SCRIPT.match(/var ORB_ACCENTS = \[([^\]]*)\]/) || [, ''])[1];
|
|
116
|
+
assert.ok(accents.length, 'the orb accent list must be findable');
|
|
117
|
+
assert.doesNotMatch(accents, /#[0-9a-f]{3,8}/i,
|
|
118
|
+
'a hardcoded hex does not follow a re-theme — derive from tokens');
|
|
119
|
+
for (const t of ['--sea', '--terracotta', '--accent', '--sun-lit', '--gold-soft']) {
|
|
120
|
+
assert.match(accents, new RegExp(`var\\(${t}\\)`), `${t} must be one of the orb accents`);
|
|
121
|
+
}
|
|
122
|
+
const glows = (SCRIPT.match(/var ORB_GLOWS = ([^;]*);/) || [, ''])[1];
|
|
123
|
+
assert.doesNotMatch(glows, /rgba?\(/, 'orb glows must derive from the accents, not literal rgba');
|
|
124
|
+
assert.equal((SCRIPT.match(/var ORB_ACCENTS/g) || []).length, 1,
|
|
125
|
+
'one palette, not two — the map and the manage cards must not drift apart');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('accents are assigned by canonical position, not by hashing the name', () => {
|
|
129
|
+
// a name hash collided: with five tokens and four projects it put three of
|
|
130
|
+
// them on the same ember and the sky stopped being readable
|
|
131
|
+
assert.match(SCRIPT, /function assignAccents/);
|
|
132
|
+
assert.doesNotMatch(SCRIPT, /hashIndex\(p\.name/,
|
|
133
|
+
'name-hashed accents collide — assign from a canonical order');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('the mark holds "bongos" and types the assistant, not the other way round', () => {
|
|
137
|
+
// `bongos` is the word a person types; the assistant is what varies. This is
|
|
138
|
+
// the same correction the landing hero needed.
|
|
139
|
+
assert.match(HUB, /<span id="markWord">bongos<\/span>/);
|
|
140
|
+
const prefixes = (SCRIPT.match(/var PREFIXES = \[([^\]]*)\]/) || [, ''])[1];
|
|
141
|
+
assert.ok(prefixes.length, 'the typed prefix list must be findable');
|
|
142
|
+
assert.doesNotMatch(prefixes, /'bongos'/, 'bongos is the constant, never a typed variant');
|
|
143
|
+
assert.match(prefixes, /'claude'/);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('the typing and the map both respect prefers-reduced-motion (C9)', () => {
|
|
147
|
+
assert.match(SCRIPT, /prefers-reduced-motion: reduce/);
|
|
148
|
+
assert.match(SCRIPT, /if \(REDUCED \|\|/, 'the typing loop must be skipped under reduced motion');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('the owner poll is view-guarded — it must not run on the map (C8)', () => {
|
|
152
|
+
assert.match(SCRIPT, /function shouldPoll\(\)[\s\S]*?currentView === 'mine'/,
|
|
153
|
+
'the settled-card refresh must only run in the view that has cards');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('catalog origins are scheme-checked before they become hrefs', () => {
|
|
157
|
+
// esc() neutralizes HTML metacharacters but NOT a `javascript:` URI, and this
|
|
158
|
+
// page drives teardown / revive / domain-attach for the signed-in owner — so
|
|
159
|
+
// script running in its origin is not a small thing. Same predicate the
|
|
160
|
+
// landing page's catalog band already uses (public-landing/index.html:544).
|
|
161
|
+
assert.match(SCRIPT, /function safeHttpUrl/);
|
|
162
|
+
assert.match(SCRIPT, /u\.protocol === 'https:' \|\| u\.protocol === 'http:'/);
|
|
163
|
+
// and no raw origin may reach an href
|
|
164
|
+
assert.doesNotMatch(SCRIPT, /a\.href = p\.origin/, 'orb hrefs must go through safeHttpUrl');
|
|
165
|
+
assert.doesNotMatch(SCRIPT, /href="' \+ esc\(p\.origin\)/, 'table hrefs must go through safeHttpUrl');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('liveness is never claimed for a catalog row the hub did not probe (C6)', () => {
|
|
169
|
+
// the public feed carries origin/name/tagline/art_url and nothing else — a
|
|
170
|
+
// chip saying "live" here would assert more than anything verified
|
|
171
|
+
assert.match(SCRIPT, /chip calm/, 'an unprobed catalog row gets the calm chip');
|
|
172
|
+
assert.doesNotMatch(SCRIPT, /chip live[^']*'\s*\+\s*esc\(p\./,
|
|
173
|
+
'no catalog row may be rendered as live');
|
|
174
|
+
// the three active outcomes stay three in the manage list
|
|
175
|
+
for (const label of ['unreachable', 'live\\?', 'live']) {
|
|
176
|
+
assert.match(SCRIPT, new RegExp(`'${label}'`), `the ${label} outcome must survive`);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('the gate is retired — no route, no files, no links into it', () => {
|
|
181
|
+
// the FILES, not the directory: git cannot track an empty dir, so a stale
|
|
182
|
+
// working tree may keep the folder while the commit is clean
|
|
183
|
+
for (const f of ['index.html', 'space.js', 'space.css']) {
|
|
184
|
+
assert.equal(fs.existsSync(path.join(ROOT, 'public-landing', 'gate', f)), false,
|
|
185
|
+
`public-landing/gate/${f} must be gone: express.static would keep serving it`);
|
|
186
|
+
}
|
|
187
|
+
const server = fs.readFileSync(path.join(ROOT, 'src', 'platform-server.js'), 'utf8');
|
|
188
|
+
assert.doesNotMatch(server, /res\.redirect\(302, '\/gate\/'\)/,
|
|
189
|
+
'the apex must serve the landing page, not redirect into the gate');
|
|
190
|
+
assert.doesNotMatch(server, /gateIndexPath/);
|
|
191
|
+
assert.doesNotMatch(HUB, /href="\/gate/);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('/create still answers — the hub links it rather than absorbing it', () => {
|
|
195
|
+
// Task 1002743's Part 3 was AMENDED by the owner on 2026-08-13 to move the
|
|
196
|
+
// "/create redirects into the hub" clause to task 1002763, which carries the
|
|
197
|
+
// port that makes it safe. ?view=new is step 1 of the four-beat rail, so
|
|
198
|
+
// redirecting /create here before the wizard lives in the hub would let a
|
|
199
|
+
// visitor name a project and then go nowhere — creation would break. The
|
|
200
|
+
// wizard also has four id collisions with this page and a replaceState that
|
|
201
|
+
// strips the query string. Until 1002763 lands, /create must keep answering.
|
|
202
|
+
assert.ok(fs.existsSync(path.join(ROOT, 'public-landing', 'create.html')));
|
|
203
|
+
assert.match(SCRIPT, /location\.assign\('\/create'/);
|
|
204
|
+
assert.match(HUB, /href="\/create"/, 'the empty state must still point at the wizard');
|
|
205
|
+
});
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
# Cloud Bongos — landing gate (interactive)
|
|
2
|
-
|
|
3
|
-
`index.html` is the Cloud Bongos landing experience — a small state machine over one
|
|
4
|
-
self-contained page (`space.css` + `space.js` for the space side; the download scene + its
|
|
5
|
-
drag physics live in the inline `<script>`), built from **drawn ATLA-style sprites**
|
|
6
|
-
(`sprites/*.png` — a `> cloud bongos` terminal, an enter key, a search bar, and download tiles)
|
|
7
|
-
refracting the backdrop through SVG displacement maps.
|
|
8
|
-
|
|
9
|
-
## The flow (tasks 1721, 1722)
|
|
10
|
-
|
|
11
|
-
Four body-class views: `view-entry` → `view-space` → `view-download` / `view-detail`.
|
|
12
|
-
|
|
13
|
-
1. **Entry — type `bongos`, in space** (`#entry`, `view-entry`). Every visit lands here: the
|
|
14
|
-
`> cloud ▊` terminal floating in the **space sky** (deep-blue → sunrise gradient, drifting
|
|
15
|
-
clouds), *not* the temple world. Typing `bongos` is the ritual on every visit; the enter key
|
|
16
|
-
lights, and pressing enter flashes through into the circles. Driven by `space.js`
|
|
17
|
-
(`entryKey` / `enterSite`), reusing the terminal + enter CSS.
|
|
18
|
-
2. **Space — the circles** (`#space` / `#space-canvas`, `view-space`). A pannable, zoomable sky
|
|
19
|
-
of project orbs, sized by number of builders, moving as a **living gravity cluster** — every
|
|
20
|
-
orb attracts every other by its mass (∝ size³), so the field swirls, bodies sling past one
|
|
21
|
-
another, and a small orb is sometimes briefly caught circling a big one like a moon (task
|
|
22
|
-
1733; see "The gravity" below). Project data is **inlined** in `space.js` (`PROJECTS`) — Caddy
|
|
23
|
-
404s `*.json` on staging — so swap in a real registry by changing `PROJECTS` + `layout()`.
|
|
24
|
-
- **Drag** empty sky to pan · **scroll** to zoom (toward the cursor).
|
|
25
|
-
- **Hover** an orb for an info card; **click** it to dive in.
|
|
26
|
-
- **Space bar** opens a search scoped to the orbs **currently in view** (client-side match
|
|
27
|
-
over name/tagline/tags now; the Claude-backed answer is a dormant seam, `aiAnswer()`, per
|
|
28
|
-
the "interaction now, AI wired next" decision).
|
|
29
|
-
- **Click empty sky** drops a new project circle.
|
|
30
|
-
3. **Download — Cloud Bongos' install page** (`#land` in `.stage`, `view-download`). Clicking the
|
|
31
|
-
Cloud Bongos orb zooms **straight** into the search + download-tiles scene over the
|
|
32
|
-
temple-world backdrop — **no type-`bongos` gate in between** (that already happened at entry).
|
|
33
|
-
`space.js` fires `cb:show-download`; the inline script reveals `#land` and runs its drag.
|
|
34
|
-
4. **Detail — other projects** (`#detail`, `view-detail`). Clicking any non-self orb opens a
|
|
35
|
-
themed "full story — coming soon" page with a link out to its live site.
|
|
36
|
-
|
|
37
|
-
**ESC** rises from the download or detail page back to the circles (`space.js` → `cb:hide-download`).
|
|
38
|
-
|
|
39
|
-
## The download scene's elastic-tether drag (task 1534)
|
|
40
|
-
|
|
41
|
-
Every download sprite (search bar, tiles, wordmark) is draggable. Grab one and it resists like
|
|
42
|
-
it's anchored; pull past the threshold and it snaps free, hovers, then drifts slowly home and
|
|
43
|
-
re-docks, carrying a faint idle bob at rest. The physics is scoped to `.stage`, so it never
|
|
44
|
-
grabs the entry terminal (which `space.js` owns). **This scene is separate from the space
|
|
45
|
-
gravity below — the download interaction is unchanged by task 1733.**
|
|
46
|
-
|
|
47
|
-
## The space gravity — a living n-body cluster (task 1733)
|
|
48
|
-
|
|
49
|
-
The project orbs on the **space** view (`view-space`) are a real gravitational simulation, all
|
|
50
|
-
in `space.js`. Each orb is a body with **mass ∝ size³** (more builders → bigger → heavier →
|
|
51
|
-
pulls harder), so the sky behaves like planets, suns and the occasional fleeting moon rather
|
|
52
|
-
than a fixed scatter. Under the hood:
|
|
53
|
-
|
|
54
|
-
- **Softened Newtonian gravity**, every pair, so a close pass *slings* instead of blowing up
|
|
55
|
-
(no singularities).
|
|
56
|
-
- A **fixed-timestep leapfrog integrator** (`physStep`) for accurate, energy-stable motion; the
|
|
57
|
-
frame loop (`tick` → `render`) interpolates between steps for smoothness.
|
|
58
|
-
- **Soft collisions** so discs bounce rather than overlap; a **soft containment well** plus a
|
|
59
|
-
**band thermostat** keep the swirl bounded and lively *forever* — energy is free to ebb and
|
|
60
|
-
flow inside a band, and only a runaway collapse or blow-up gets a gentle nudge back.
|
|
61
|
-
- Net drift is removed each step, so the cluster stays centred on screen (and stays put when a
|
|
62
|
-
visitor adds a new orb, which is flung into the swirl).
|
|
63
|
-
|
|
64
|
-
Verified stable over a 60-minute headless simulation (no NaN, never off-screen, never
|
|
65
|
-
interpenetrating). It replaced the earlier "drift around fixed anchors" motion.
|
|
66
|
-
|
|
67
|
-
### Tuning the gravity
|
|
68
|
-
|
|
69
|
-
All knobs live in the `GRAV` block near the top of `space.js`:
|
|
70
|
-
|
|
71
|
-
| You want… | Edit (in `GRAV`) |
|
|
72
|
-
|--------------------------------------|---------------------------------------------------|
|
|
73
|
-
| Livelier / faster swirl | `SWIRL` ↑ (or `THERMO_LO` ↑) |
|
|
74
|
-
| Calmer, slower motion | `SWIRL` ↓ / `THERMO_LO` ↓ |
|
|
75
|
-
| Tighter cluster (less panning) | `CONTAIN_R` ↓ |
|
|
76
|
-
| Stronger pull between orbs | `G` ↑ |
|
|
77
|
-
| Fewer hard slingshots on near-misses | `SOFTEN` ↑ |
|
|
78
|
-
| Bouncier contacts (→ elastic) | `REST` → 1 |
|
|
79
|
-
| Sharper size→mass contrast | lower `R_REF`, or raise the `massFor` exponent |
|
|
80
|
-
|
|
81
|
-
Turn it off (freeze the orbs): comment out `initOrbits()` in `layout()` and the
|
|
82
|
-
`requestAnimationFrame(tick)` at the bottom of the IIFE — pan, zoom, hover, dive, search and the
|
|
83
|
-
table all keep working.
|
|
84
|
-
|
|
85
|
-
> **Live:** password-locked at **staging.cloudbongos.com** (the future landing page, parked
|
|
86
|
-
> for review). Setup + the password live in [`docs/recipes/cloudbongos-staging-gate.md`](../../../docs/recipes/cloudbongos-staging-gate.md).
|
|
87
|
-
> Branding (logo coin / wordmark / mascot / code-glyphs) is still being finished and is
|
|
88
|
-
> intentionally untouched here.
|
|
89
|
-
|
|
90
|
-
## Preview it locally
|
|
91
|
-
|
|
92
|
-
The page links to art one folder up (`../world/assets/…`), so it must be served from the
|
|
93
|
-
**`brand/cloud-bongos/`** folder — not from `gate/` itself, or those paths break.
|
|
94
|
-
|
|
95
|
-
**Easiest (Claude Code preview):** start the `gate-preview` server in `.claude/launch.json`,
|
|
96
|
-
then open `http://127.0.0.1:8137/gate/index.html`.
|
|
97
|
-
|
|
98
|
-
**From a terminal** (repo root): `python3 -m http.server --directory brand/cloud-bongos 8137`,
|
|
99
|
-
then open <http://127.0.0.1:8137/gate/index.html>.
|
|
100
|
-
|
|
101
|
-
Use a Chromium browser (Chrome/Edge) — the live-glass refraction relies on `backdrop-filter`
|
|
102
|
-
with an SVG displacement map. Safari ignores the displacement and falls back to a plain
|
|
103
|
-
frost, which still looks fine.
|
|
104
|
-
|
|
105
|
-
## Tuning the drag
|
|
106
|
-
|
|
107
|
-
All behaviour is in the single `<script>` at the bottom of `index.html`, which opens with a
|
|
108
|
-
commented **TUNING** block. Common tweaks:
|
|
109
|
-
|
|
110
|
-
| You want… | Edit (in the TUNING block) |
|
|
111
|
-
|------------------------------------------|-----------------------------------|
|
|
112
|
-
| Harder / easier to pull a pane free | `THRESHOLD` — ↑ harder, ↓ easier |
|
|
113
|
-
| How far a tap can move before it's a drag | `DEADZONE` — px |
|
|
114
|
-
| Freed panes wait longer before returning | `HOVER` — milliseconds |
|
|
115
|
-
| Slower, more drifty return | `DRIFT_MAXV` — ↓ slower |
|
|
116
|
-
| Wider, more wandering return path | `WMAG` ↑ and/or `TURN` ↓ |
|
|
117
|
-
| More / less idle "breathing" | `BOB_X`, `BOB_Y` (px) — `0` = off |
|
|
118
|
-
|
|
119
|
-
### Which elements are draggable
|
|
120
|
-
|
|
121
|
-
The drag targets **`.mark`** (the wordmark) + every **`.el`** sprite pane (terminal, enter,
|
|
122
|
-
search, tiles). To add one, give it the `el` class (or push it into the `els` array); to
|
|
123
|
-
exclude one, filter it out of `els`.
|
|
124
|
-
|
|
125
|
-
### Why it does not fight the other animations
|
|
126
|
-
|
|
127
|
-
The drag only ever applies `transform: translate(…)` to each pane, and it owns that property
|
|
128
|
-
exclusively — so the float/reveal animations were replaced by the drag's own idle bob, and
|
|
129
|
-
the enter key's reveal uses opacity + blur only (no transform). The **scene** transforms
|
|
130
|
-
(the dive's scale/translate) live on the parent `.gate`/`.land` containers, so they compose
|
|
131
|
-
with the per-pane drag instead of clobbering it. When a scene settles, `measure()` re-runs
|
|
132
|
-
so a freshly-revealed pane drags from the right home.
|
|
133
|
-
|
|
134
|
-
### Turn the drag off
|
|
135
|
-
|
|
136
|
-
Delete the second `<script>` IIFE (the drag) — the type→dive interaction and all the art
|
|
137
|
-
keep working untouched.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|