@bongos/core 1.19.113 → 1.19.115

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.
@@ -202,10 +202,6 @@ ALLOWLIST=(
202
202
  'tests/auth_anthropic_identity.mjs'
203
203
  'modules/dev-box/app/test/unit.mjs'
204
204
 
205
- # The public landing page links the open-source repo by URL — repo-hosting
206
- # identity (the provision-staging.sh class), not a builder identity.
207
- 'public-landing/create.html'
208
-
209
205
  # The mirror-redaction floor: PRIOR_INSTANCE_FLOOR carries the prior
210
206
  # instance's owner literal precisely so packaging can SCRUB it from every
211
207
  # published core — the anti-leak spec, not a runtime hardcode (task 1002640).
@@ -23,7 +23,7 @@ const oauth = require(path.join(__dirname, 'oauth-secret.js')); // validateC
23
23
  // ── PURE core (exported for tests) ───────────────────────────────────────────
24
24
 
25
25
  // The canonical step sequence now lives in ONE place — the provisioning module — so the
26
- // CLI (here), the branded /create page, and the hall wizard can't drift (task 2085; goal
26
+ // CLI (here) and the hub's ?view=new wizard can't drift (task 2085; goal
27
27
  // 35 / criterion 165: "the canonical step sequence is the source of truth; skill/CLI/hall
28
28
  // are forms over it"). The web forms read it over GET /provisioning/onboard-plan; the CLI
29
29
  // requires it directly. Re-exported below so `bongos onboard` + tests keep the same API.
package/src/module-api.js CHANGED
@@ -653,8 +653,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
653
653
  // landed since 1.19.111 with no explicit bump. run 31814245230. (task 1002620)
654
654
  // 1.19.113 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
655
655
  // landed since 1.19.112 with no explicit bump. run 31816239246. (task 1002620)
656
+ // 1.19.114 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
657
+ // landed since 1.19.113 with no explicit bump. run 31816599678. (task 1002620)
658
+ // 1.19.115 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
659
+ // landed since 1.19.114 with no explicit bump. run 31825824596. (task 1002620)
656
660
  // ---------------------------------------------------------------------------
657
- const CORE_VERSION = '1.19.113'; // CI auto-patch carrier (ADR 0161); see changelog above
661
+ const CORE_VERSION = '1.19.115'; // CI auto-patch carrier (ADR 0161); see changelog above
658
662
 
659
663
  // A namespaced logger so a module's log lines are attributable + consistent.
660
664
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -184,13 +184,21 @@ function buildPlatformApp() {
184
184
  if (!html) return next();
185
185
  res.type('html').send(html);
186
186
  });
187
- // The branded project-creation experience (goal 35, task 2081) — a sibling
188
- // landing page at a clean /create URL, branding-injected like the landing.
187
+ // The create wizard was absorbed into the hub's ?view=new view (task 1002763;
188
+ // direction §4 — "/create redirects to ?view=new"). This route only carries
189
+ // visitors — and any ?name= seed the old links still send — into it. 302, not
190
+ // 301: /create is an OAuth return target in the wild, and a browser-cached
191
+ // permanent redirect would outlive any future change of shape.
189
192
  app.get('/create', (req, res, next) => {
190
193
  if (!isApexHost(req)) return next();
191
- const html = landingPage('create.html');
192
- if (!html) return next();
193
- res.type('html').send(html);
194
+ // rebuilt, not spliced: an inbound ?view= must not ride along (the hub
195
+ // reads the FIRST view param — a duplicate would bet the route on that
196
+ // parser detail), and an empty query must not leave a bare trailing '&'
197
+ const qi = req.url.indexOf('?');
198
+ const rest = new URLSearchParams(qi >= 0 ? req.url.slice(qi + 1) : '');
199
+ rest.delete('view');
200
+ const tail = rest.toString();
201
+ res.redirect(302, '/projects?view=new' + (tail ? '&' + tail : ''));
194
202
  });
195
203
  // THE PROJECT HUB (task 1002743) — one surface holding every project-shaped
196
204
  // action: the map of what exists (the orb sky the gate used to own, plus the
@@ -144,8 +144,9 @@ const PAIRS = [
144
144
  ['.btn.ghost:hover label', 'var(--accent-deep)', CARD, 4.5],
145
145
  ['.btn.danger label', 'var(--err)', CARD, 4.5],
146
146
  // Inline links: cosmos.css recolors its own to --sun-lit in dark (:179-262), but
147
- // create.html has ELEVEN --accent-deep consumers and zero dark CSS rules (its only
148
- // data-mode hits are the JS toggle), so there the token itself must carry dark.
147
+ // the ?view=new wizard styles (absorbed from create.html, task 1002763) carry
148
+ // eleven --accent-deep consumers and zero dark rules of their own, so there the
149
+ // token itself must carry dark.
149
150
  ['inline link on a card', 'var(--accent-deep)', CARD, 4.5],
150
151
  ['inline link on the page ground', 'var(--accent-deep)', 'var(--bg)', 4.5],
151
152
  ['code chip on --bg-deep', 'var(--accent-deep)', 'var(--bg-deep)', 4.5],
@@ -172,11 +173,13 @@ const PAIRS = [
172
173
  ['.projMsg.ok', 'var(--ok)', CARD, 4.5],
173
174
  ['.projMsg.err / .confirmRow', 'var(--err)', CARD, 4.5],
174
175
  // chrome: wordmark + footer sit on the page ground, not on a card
175
- // create.html page-local grounds — it carries its own <style> and its own
176
- // darkest tints, so cosmos.css's pairs alone do not cover /create
177
- ['create.html .ghrow .priv 11px', 'var(--ink-soft)', 'color-mix(in srgb,var(--ink) 8%,var(--bg-deep))', 4.5],
178
- ['create.html ::placeholder', 'var(--ink-faint)', CARD, 4.5],
179
- ['create.html .ghlist .hint on deep', 'var(--ink-faint)', 'var(--bg-deep)', 4.5],
176
+ // wizard grounds (projects.html ?view=new, absorbed from create.html in task
177
+ // 1002763) — its panels carry their own darkest tints, so cosmos.css's pairs
178
+ // alone do not cover them
179
+ ['?view=new .ghrow .priv 11px', 'var(--ink-soft)', 'color-mix(in srgb,var(--ink) 8%,var(--bg-deep))', 4.5],
180
+ ['--on-ok on an --ok fill', 'var(--on-ok)', 'var(--ok)', 4.5],
181
+ ['?view=new input ::placeholder', 'var(--ink-faint)', CARD, 4.5],
182
+ ['?view=new .ghlist .hint on deep', 'var(--ink-faint)', 'var(--bg-deep)', 4.5],
180
183
  ['--chrome-accent wordmark', 'var(--chrome-accent)', 'var(--page-bg)', 4.5],
181
184
  ['--chrome-ink', 'var(--chrome-ink)', 'var(--page-bg)', 4.5],
182
185
  ['--foot-ink footer copy', 'var(--foot-ink)', 'var(--page-bg)', 4.5],
@@ -127,7 +127,7 @@ test('platform-identity joined the roster (task 1002322) — the catalog territo
127
127
  'modules/platform-identity/routes/projects.js', // the feed + invite routes
128
128
  'modules/status-ui/public/explore.js', // orb render-type surface
129
129
  'modules/hall-ui/public/my-projects.js', // owner settings / applications queue
130
- 'public-landing/create.html', // the project-detail creation step
130
+ 'public-landing/projects.html', // the creation step, ?view=new since task 1002763
131
131
  'modules/provisioning/', // module-bundle selection at provision time
132
132
  ]) {
133
133
  assert.equal(covers(touch), true, `wall does not cover ${touch}`);
package/tests/onboard.mjs CHANGED
@@ -86,18 +86,19 @@ test('every render surface carries the terminal hall step (task 1002703)', () =>
86
86
  const cli = read('scripts/gds/onboard.js');
87
87
  assert.match(cli, /builders hall/i, 'the CLI walkthrough ends at the hall');
88
88
  assert.match(cli, /bongos dev/, 'the CLI offers the local-hall one-liner when no domain');
89
- const createHtml = read('public-landing/create.html');
90
- const enterHall = createHtml.match(/data-onboard-step="enter-hall"[^]*?<\/li>/g) || [];
91
- assert.ok(enterHall.length >= 1, '/create tags the enter-hall step');
92
- assert.match(enterHall.join(''), /\/builders/, '/create points at the hall address');
93
- assert.match(enterHall.join(''), /bongos dev/, '/create offers the local hall when no domain');
89
+ const hubHtml = read('public-landing/projects.html');
90
+ const enterHall = hubHtml.match(/data-onboard-step="enter-hall"[^]*?<\/li>/g) || [];
91
+ assert.ok(enterHall.length >= 1, 'the ?view=new wizard tags the enter-hall step');
92
+ assert.match(enterHall.join(''), /\/builders/, 'the wizard points at the hall address');
93
+ assert.match(enterHall.join(''), /bongos dev/, 'the wizard offers the local hall when no domain');
94
94
  });
95
95
 
96
- test('the /create done panel is plan-driven, manifest-driven, and owner-honest (task 1002694)', () => {
96
+ test('the wizard done panel is plan-driven, manifest-driven, and owner-honest (task 1002694)', () => {
97
97
  // Gaps 2/3/4/14 of the project-creation audit (docs/audits/project-creation-gap-list.md).
98
+ // The wizard lives inside the hub page since task 1002763.
98
99
  const { readFileSync } = require('node:fs');
99
100
  const path = require('node:path');
100
- const src = readFileSync(path.join(import.meta.dirname, '..', 'public-landing/create.html'), 'utf8');
101
+ const src = readFileSync(path.join(import.meta.dirname, '..', 'public-landing/projects.html'), 'utf8');
101
102
  // gap 2 (idea 1000675): the checklist renders FROM the canonical plan endpoint
102
103
  assert.match(src, /provisioning\/onboard-plan/, 'the done panel fetches the canonical plan');
103
104
  // gap 3 (idea 1000676): the ADR 0133 one-click manifest flow has a UI caller again
@@ -105,12 +106,14 @@ test('the /create done panel is plan-driven, manifest-driven, and owner-honest (
105
106
  assert.match(src, /github-app\/status/, 'and polls its progress to reflect live state');
106
107
  // gap 4: the operator-only control-plane command never reaches a new owner
107
108
  assert.doesNotMatch(src, /oauth-secret place/, 'owners are never handed the control-plane placement command');
108
- // gap 14 (idea 1000684): repo visibility is stated, with the private path named
109
- assert.match(src, /public/, 'visibility is stated');
110
- assert.match(src, /adopt/i, 'the private path (adopt your own private repo) is named');
109
+ // gap 14 (idea 1000684): repo visibility is stated, with the private path named.
110
+ // Landmarks unique to the wizard — bare /public/ and /adopt/i match hub copy
111
+ // that has nothing to do with it, which would leave this guard vacuous.
112
+ assert.match(src, /public stays public, private stays private/, 'visibility is stated');
113
+ assert.match(src, /id="c-adopt"/, 'the private path (adopt your own private repo) is named');
111
114
  });
112
115
 
113
- test('the /create gate trust link points at a live artifact, not the dead private-repo URL (task 1002705)', () => {
116
+ test('the wizard gate trust link points at a live artifact, not the dead private-repo URL (task 1002705)', () => {
114
117
  // Gap 1 of the project-creation audit: the gate linked a nonexistent private repo
115
118
  // (404 even authenticated — ADR 0167 names it). No public code artifact exists yet
116
119
  // (the ADR 0099 mirror is dormant until ~2026-11, and both npm scopes 404 anonymously
@@ -118,7 +121,7 @@ test('the /create gate trust link points at a live artifact, not the dead privat
118
121
  // "open source" is the license itself.
119
122
  const { readFileSync } = require('node:fs');
120
123
  const path = require('node:path');
121
- const src = readFileSync(path.join(import.meta.dirname, '..', 'public-landing/create.html'), 'utf8');
124
+ const src = readFileSync(path.join(import.meta.dirname, '..', 'public-landing/projects.html'), 'utf8');
122
125
  assert.doesNotMatch(src, /Claude-OTB/, 'the dead repo link is gone');
123
126
  assert.match(src, /https:\/\/www\.gnu\.org\/licenses\/agpl-3\.0/, 'the trust link resolves (the AGPL-3.0 license text)');
124
127
  });
@@ -168,19 +171,20 @@ test('onboardPlan is the SAME function the provisioning module owns (single sour
168
171
  assert.equal(ob.onboardPlan, canonical, 'scripts/gds/onboard.js must re-export the module’s onboardPlan');
169
172
  });
170
173
 
171
- test('the web form (/create) declares EXACTLY the canonical human steps — drift guard', () => {
174
+ test('the web form (?view=new) declares EXACTLY the canonical human steps — drift guard', () => {
172
175
  // goal 35 / criterion 165: "the canonical step sequence is the source of truth;
173
176
  // skill/CLI/web are forms over it." The web form tags its human-step <li>s with
174
177
  // data-onboard-step="<key>"; this asserts that set equals the plan's human-step keys,
175
178
  // so adding/removing/renaming a human step in the single source fails CI until every
176
179
  // form is updated (and vice-versa) — the sequence can no longer drift between forms.
177
- // (The hall new-project wizard was removed in task 1002675 — project creation lives
178
- // on the apex /create flow; the skill + CLI + /create remain the canonical forms.)
180
+ // (The hall new-project wizard was removed in task 1002675, and the apex /create
181
+ // page was absorbed into the hub's ?view=new view in task 1002763; the skill +
182
+ // CLI + the hub wizard remain the canonical forms.)
179
183
  const { readFileSync } = require('node:fs');
180
184
  const path = require('node:path');
181
185
  const humanKeys = ob.onboardPlan().filter((s) => s.kind === 'human').map((s) => s.key).sort();
182
186
  const surfaces = {
183
- '/create landing page': 'public-landing/create.html',
187
+ '/projects?view=new wizard': 'public-landing/projects.html',
184
188
  };
185
189
  for (const [name, rel] of Object.entries(surfaces)) {
186
190
  const src = readFileSync(path.join(import.meta.dirname, '..', rel), 'utf8');
@@ -121,16 +121,29 @@ test('the apex host serves the landing page — not the game — on a game-less
121
121
  assert.doesNotMatch(res.body, /Create a project/);
122
122
  });
123
123
 
124
- test('the apex host serves the branded /create page (goal 35, task 2081)', async () => {
125
- // The project-creation experience is a platform-level page on the apex,
126
- // branding-injected exactly like the landing (same landingPage memo).
124
+ test('/create redirects into the hub wizard, seed intact (task 1002763)', async () => {
125
+ // The wizard was absorbed into /projects?view=new (direction §4); /create
126
+ // survives only as a 302 so old links and OAuth returns keep working. The
127
+ // query string rides along — /create?name= was the seeding entry point.
127
128
  const res = await request('/create', 'game.test.local');
128
- assert.equal(res.status, 200);
129
- assert.match(res.headers['content-type'] || '', /text\/html/);
130
- assert.match(res.body, /window\.__BRANDING__=/);
131
- assert.match(res.body, /Create a project/);
129
+ assert.equal(res.status, 302);
130
+ assert.equal(res.headers.location, '/projects?view=new');
131
+ // the query is rebuilt (URLSearchParams re-encodes space as '+', which the
132
+ // hub's URLSearchParams reading decodes right back), and an inbound view=
133
+ // never rides along to fight the one this route asserts
134
+ const seeded = await request('/create?name=Sky%20Farm&view=mine', 'game.test.local');
135
+ assert.equal(seeded.status, 302);
136
+ assert.equal(seeded.headers.location, '/projects?view=new&name=Sky+Farm');
137
+ const empty = await request('/create?', 'game.test.local');
138
+ assert.equal(empty.headers.location, '/projects?view=new');
139
+ // the branded wizard itself now ships inside the hub page
140
+ const hub = await request('/projects', 'game.test.local');
141
+ assert.equal(hub.status, 200);
142
+ assert.match(hub.headers['content-type'] || '', /text\/html/);
143
+ assert.match(hub.body, /window\.__BRANDING__=/);
144
+ assert.match(hub.body, /Create a project/);
132
145
  // the wizard files the request against the provisioning API, nothing else
133
- assert.match(res.body, /provisioning\/instances/);
146
+ assert.match(hub.body, /provisioning\/instances/);
134
147
  });
135
148
 
136
149
  test('R51: an API-shaped 404 still gets a self-describing JSON body', async () => {
@@ -212,17 +212,85 @@ test('the gate is retired — no route, no files, no links into it', () => {
212
212
  assert.doesNotMatch(HUB, /href="\/gate/);
213
213
  });
214
214
 
215
- test('/create still answers — the hub links it rather than absorbing it', () => {
216
- // Task 1002743's Part 3 was AMENDED by the owner on 2026-08-13 to move the
217
- // "/create redirects into the hub" clause to task 1002763, which carries the
218
- // port that makes it safe. ?view=new is step 1 of the four-beat rail, so
219
- // redirecting /create here before the wizard lives in the hub would let a
220
- // visitor name a project and then go nowhere — creation would break. The
221
- // wizard also has four id collisions with this page and a replaceState that
222
- // strips the query string. Until 1002763 lands, /create must keep answering.
223
- assert.ok(fs.existsSync(path.join(ROOT, 'public-landing', 'create.html')));
224
- assert.match(SCRIPT, /location\.assign\('\/create'/);
225
- assert.match(HUB, /href="\/create"/, 'the empty state must still point at the wizard');
215
+ test('/create is absorbed — the wizard lives here and the route only redirects (task 1002763)', () => {
216
+ // Task 1002743 stopped at step 1: the hub named a project and navigated to
217
+ // /create?name=. This task carries the absorption the owner amended into it
218
+ // (direction §4: "/create redirects to ?view=new"): the wizard's six panels
219
+ // are real markup inside the new view (C7), create.html is deleted — the
220
+ // gate precedent above: express.static would keep serving the raw file —
221
+ // and the name handoff became an in-page step advance.
222
+ assert.equal(fs.existsSync(path.join(ROOT, 'public-landing', 'create.html')), false,
223
+ 'create.html must be gone: express.static would keep serving it raw and unbranded');
224
+ assert.doesNotMatch(SCRIPT, /location\.assign\('\/create'/,
225
+ 'naming a project advances the wizard in place — no navigation');
226
+ assert.doesNotMatch(HUB, /href="\/create"/, 'no link may point at the retired route');
227
+ assert.match(HUB, /href="\?view=new"/, 'in-page links carry the view query instead');
228
+ const server = fs.readFileSync(path.join(ROOT, 'src', 'platform-server.js'), 'utf8');
229
+ assert.match(server, /res\.redirect\(302, '\/projects\?view=new'/,
230
+ '/create must 302 into the hub (302, not 301: the URL is an OAuth return target in the wild)');
231
+ for (const id of ['panel-gate', 'panel-name', 'panel-home', 'panel-address', 'panel-review', 'panel-done']) {
232
+ assert.match(HUB, new RegExp(`id="${id}"`), `the wizard's ${id} ships as real markup (C7)`);
233
+ }
234
+ });
235
+
236
+ test('the absorbed wizard sheds its two URL landmines — /gate/ and the whole-query strip', () => {
237
+ // create.html carried a document-level Escape that navigated to the retired
238
+ // /gate/ (a 404 the href-only guard above cannot see: it was a
239
+ // location.assign, not an href) and a replaceState that rewrote the URL to
240
+ // location.pathname — which on this page would destroy ?view=new on load.
241
+ assert.doesNotMatch(SCRIPT, /['"]\/gate\//,
242
+ 'no code path may navigate into the retired gate');
243
+ assert.doesNotMatch(SCRIPT, /history\.replaceState\(null, '', location\.pathname\)/,
244
+ "the hub's '?view=' + v rewrite is the only URL writer on this page");
245
+ });
246
+
247
+ test('no id is declared twice — the wizard merge had four collisions to clear', () => {
248
+ // modeToggle, who, signin, gateAlert all existed on BOTH pages; a duplicate
249
+ // id makes the second node unreachable by getElementById and the first
250
+ // silently wins. Scan the real markup only (JS builds ids like `dom-<iid>`
251
+ // from fragments that would false-positive here).
252
+ const afterHead = HUB.slice(HUB.indexOf('</script>') + '</script>'.length);
253
+ const markup = afterHead.slice(0, afterHead.indexOf('<script>'));
254
+ assert.ok(markup.includes('id="view-new"'), 'the markup slice must span the views');
255
+ const ids = [...markup.matchAll(/\sid="([^"]+)"/g)].map((m) => m[1]);
256
+ const seen = new Set();
257
+ const dupes = ids.filter((id) => (seen.has(id) ? true : (seen.add(id), false)));
258
+ assert.deepEqual(dupes, [], `duplicate ids in the hub markup: ${dupes}`);
259
+ });
260
+
261
+ test('the new view balances its own tree — the map-only walkers never covered it', () => {
262
+ // div-balance (:535) counts view-map only, and every openAncestorsAt needle
263
+ // lives there too; a lopsided div in the wizard block would be caught by
264
+ // nothing else while silently reparenting the footer.
265
+ const start = HUB.indexOf('<section id="view-new"');
266
+ const end = HUB.indexOf('<div class="foot"');
267
+ assert.ok(start !== -1 && end > start, 'need the view-new span');
268
+ const wiz = HUB.slice(start, end);
269
+ assert.equal((wiz.match(/<div\b/g) || []).length, (wiz.match(/<\/div>/g) || []).length,
270
+ 'every div opened in #view-new closes in #view-new');
271
+ assert.equal((wiz.match(/<section\b/g) || []).length, (wiz.match(/<\/section>/g) || []).length,
272
+ 'every section opened in #view-new closes in #view-new');
273
+ });
274
+
275
+ test('C8 — the wizard polls stop dead off the new view, like the sky off the map', () => {
276
+ // create.html's progress watch (5s) and manifest poll (4s/8s) had NO view
277
+ // guard — merged unguarded they would poll a hidden view for up to 33
278
+ // minutes. Both ticks must consult the same gate the sky consults.
279
+ assert.match(SCRIPT, /function wizPollActive\(\)/, 'the wizard needs its own stage guard');
280
+ assert.match(SCRIPT, /!document\.hidden && currentView === 'new'/,
281
+ 'the guard is hidden-tab aware and view-scoped');
282
+ const gatedTicks = (SCRIPT.match(/if \(!wizPollActive\(\)\)/g) || []).length;
283
+ assert.ok(gatedTicks >= 2, `both wizard ticks (progress + manifest) must check the guard, found ${gatedTicks}`);
284
+ assert.match(SCRIPT, /if \(v === 'new'\) wizard\.enter\(focus\); else wizard\.leave\(\);/,
285
+ 'setView hands the view to the wizard exactly like it starts/stops the sky');
286
+ });
287
+
288
+ test('the wizard never steals focus on boot — focus moves only on deliberate steps', () => {
289
+ // create.html's show() unconditionally focused each panel's h1, and its boot
290
+ // always called show() — merged as-is, every hub load would rip focus into a
291
+ // hidden view. The hub rule (setView's `focus` flag) now reaches show() too.
292
+ assert.match(SCRIPT, /function show\(step, moveFocus\)/, 'show() must carry the focus flag');
293
+ assert.match(SCRIPT, /if \(moveFocus\)/, 'and only focus when it is set');
226
294
  });
227
295
 
228
296
  /* ── The platform's own entry (task 1002760) ────────────────────────────────
@@ -702,3 +770,72 @@ test('the chip-freshness tick spends no requests — it re-renders what it has (
702
770
  assert.match(fn, /document\.hidden/, 'and does nothing in a hidden tab');
703
771
  assert.doesNotMatch(fn, /fetch\(/, 'zero requests — the re-fetch belongs to entry and tab return only');
704
772
  });
773
+
774
+ /* ── The living sky (task 1002762) ──────────────────────────────────────────
775
+ The retired gate's n-body cluster + pan/zoom/hover/spotlight, rewritten for
776
+ the hub: one camera layer, physics in ~110 guarded lines, and the mock's
777
+ static field as the reduced-motion and no-JS state. These pin the C8/C9
778
+ contract and the structural topology the camera depends on. ── */
779
+
780
+ test('the sky camera is one layer inside the field, outside the sentinels', () => {
781
+ const stack = openAncestorsAt(HUB, 'id="skyCam"');
782
+ assert.ok(stack.includes('view-map') && stack.includes('orbField'),
783
+ 'the camera lives in the orb field. open: ' + stack.join(' > '));
784
+ assert.ok(!stack.includes('sub-table-body'), 'and never inside the table');
785
+ const selfStack = openAncestorsAt(HUB, 'id="selfOrb"');
786
+ assert.ok(selfStack.includes('skyCam'), 'the platform orb pans and zooms with everyone else');
787
+ const camAt = HUB.indexOf('id="skyCam"');
788
+ const selfAt = HUB.indexOf('id="selfOrb"');
789
+ const sentinelAt = HUB.lastIndexOf('<!--apex-only:start-->', selfAt);
790
+ assert.ok(camAt !== -1 && camAt < sentinelAt && sentinelAt < selfAt,
791
+ 'the camera opens BEFORE the orb sentinel — stripApexOnly() must never eat its open tag');
792
+ const hoverStack = openAncestorsAt(HUB, 'id="hoverCard"');
793
+ assert.ok(hoverStack.includes('orbField') && !hoverStack.includes('skyCam'),
794
+ 'the hover card sits outside the camera so it never scales or pans');
795
+ });
796
+
797
+ test('the sim stops dead when the sky is not on stage (C8, C9)', () => {
798
+ const active = (SCRIPT.match(/function skyActive\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
799
+ assert.ok(active, 'skyActive() must exist');
800
+ for (const guard of ['REDUCED', 'document.hidden', "currentView === 'map'", 'sub-sky-body']) {
801
+ assert.ok(active.includes(guard), `skyActive must check ${guard}`);
802
+ }
803
+ const tick = (SCRIPT.match(/function skyTick\(now\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
804
+ assert.ok(tick, 'skyTick() must exist');
805
+ assert.match(tick, /if \(!skyActive\(\)\) return/, 'a tick on a hidden sky ends the loop, not just the work');
806
+ assert.doesNotMatch(tick, /fetch\(/, 'the sky spends no requests (C6+C8)');
807
+ const step = (SCRIPT.match(/function physStep\(dt\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
808
+ assert.ok(step, 'physStep() must exist');
809
+ assert.doesNotMatch(step, /fetch\(/);
810
+ });
811
+
812
+ test('a drag is a pan and a click is a click — the two never blur', () => {
813
+ assert.match(SCRIPT, /var SKY_DEADZONE = 5/, 'the 5px Euclidean deadzone from the gate');
814
+ // mutation-found gap: a bare /skyPanned/ match was satisfied by the flag's
815
+ // declaration alone — pin the CONSUME site and the deferred clear instead
816
+ assert.match(SCRIPT, /if \(skyPanned\) \{ ev\.preventDefault\(\); return; \}/,
817
+ 'the delegated click handler consumes the pan flag AND still cancels the link');
818
+ assert.match(SCRIPT, /setTimeout\(function \(\) \{ skyPanned = false; \}, 0\)/,
819
+ 'the flag clears a tick after the pan ends, so a keyboard Enter is never eaten');
820
+ assert.match(SCRIPT, /pointercancel/, 'an interrupted touch must not strand the grab state');
821
+ assert.match(SCRIPT, /panId/, 'one pointer owns the drag — a second finger cannot hijack it');
822
+ });
823
+
824
+ test('anchors are never read from a hidden field', () => {
825
+ // review CRITICAL: offsetLeft/Top and clientWidth all read 0 under
826
+ // [hidden]{display:none} — anchoring to zeros paints every orb at
827
+ // slot + physics position when the sky returns. The rebuild must defer.
828
+ const fn = (SCRIPT.match(/function rebuildSkyBodies\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
829
+ assert.ok(fn, 'rebuildSkyBodies() must exist');
830
+ assert.match(fn, /clientWidth\) \{ skyNeedsRebuild = true; return; \}/,
831
+ 'an unlaid-out field defers the rebuild instead of anchoring to zeros');
832
+ assert.match(SCRIPT, /if \(skyNeedsRebuild\) rebuildSkyBodies\(\);/,
833
+ 'the deferred rebuild is consumed when the sky comes back on stage');
834
+ });
835
+
836
+ test('zoom heads for the cursor and survives line-mode wheels', () => {
837
+ assert.match(SCRIPT, /deltaMode/, 'Firefox line-mode deltas must be normalized');
838
+ assert.match(SCRIPT, /Math\.exp\(/, 'exponential zoom — symmetric in and out');
839
+ assert.ok(COSMOS.includes('transform-origin:0 0'),
840
+ 'the camera algebra assumes the layer scales about its origin');
841
+ });
@@ -14,8 +14,9 @@
14
14
  // intent_conflict, not_teardownable, bad_domain, domain_taken,
15
15
  // domain_already_set, domain_not_attachable are all SAID, never swallowed)
16
16
  // • an instance with no domain gets an HONEST no-address state, not a blank
17
- // • create.html's poll give-up now points at /projects — the false "the hall
18
- // has it" stop (the hall my-projects card was removed in task 1002675) is gone
17
+ // • the wizard's poll give-up points at the projects view — the false "the
18
+ // hall has it" stop (the hall card was removed in task 1002675) is gone.
19
+ // (create.html itself was absorbed into ?view=new in task 1002763.)
19
20
  //
20
21
  // Run: node --test tests/public_landing_projects.mjs
21
22
 
@@ -106,17 +107,20 @@ test('a domainless instance gets an honest no-address state', async () => {
106
107
  assert.match(body, /[Nn]o address yet/);
107
108
  });
108
109
 
109
- test('the empty state points a new owner at /create — never at a hall', async () => {
110
+ test('the empty state points a new owner at the wizard view — never at a hall', async () => {
110
111
  const { body } = await request('/projects', 'game.test.local');
111
- assert.match(body, /href="\/create"/);
112
+ // /create was absorbed into ?view=new (task 1002763); the CTA moves in-page
113
+ assert.match(body, /href="\?view=new"/);
114
+ assert.doesNotMatch(body, /href="\/create"/, 'no link may point at the retired route');
112
115
  // the hall my-projects card was REMOVED (task 1002675; hall my-projects is
113
116
  // federation-only) — this page must not send an owner there
114
117
  assert.doesNotMatch(body, /Enter the Hall/i);
115
118
  });
116
119
 
117
- test('create.html: the poll give-up points at /projects — "the hall has it" is dead', async () => {
118
- const { body } = await request('/create', 'game.test.local');
119
- assert.match(body, /\/projects/, 'the give-up path must hand the owner the projects page');
120
+ test('the watch give-up hands the owner their projects view — "the hall has it" is dead', async () => {
121
+ // the wizard (and its 33-minute watch cap) lives on /projects now
122
+ const { body } = await request('/projects', 'game.test.local');
123
+ assert.match(body, /your projects page/, 'the give-up path must hand the owner the projects view');
120
124
  assert.doesNotMatch(body, /the hall has it/, 'the false poll-stop copy must be gone');
121
125
  });
122
126
 
@@ -151,7 +155,8 @@ test('the stamp is derived from CONTENT, so it changes only when the file does',
151
155
  });
152
156
 
153
157
  test('every landing surface is stamped, not just the one that broke', async () => {
154
- for (const p of ['/', '/projects', '/create']) {
158
+ // /create dropped from the loop: it 302s since task 1002763 (no body to stamp)
159
+ for (const p of ['/', '/projects']) {
155
160
  const res = await request(p);
156
161
  if (res.status !== 200) continue;
157
162
  assert.doesNotMatch(res.body, /(href|src)="\/assets\/[^"?]+"/,
@@ -163,8 +168,8 @@ test('routes and API paths are never mistaken for assets', async () => {
163
168
  // stamping resolves the URL against public-landing/ — a route names no file,
164
169
  // so it must come back untouched rather than acquiring a meaningless ?v=
165
170
  const res = await request('/projects');
166
- assert.match(res.body, /href="\/create"/, '/create is a route, not an asset');
167
- assert.doesNotMatch(res.body, /href="\/create\?v=/);
171
+ assert.match(res.body, /href="\/"/, "'/' is a route, not an asset");
172
+ assert.doesNotMatch(res.body, /href="\/\?v=/);
168
173
  assert.doesNotMatch(res.body, /href="\/projects\?v=/);
169
174
  assert.doesNotMatch(res.body, /\/api\/bongos\/[^"]*\?v=[0-9a-f]{8}/,
170
175
  'API paths must never be stamped');