@bongos/core 1.19.694 → 1.19.696

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/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.694'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.696'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -92,19 +92,20 @@ test('AUDIT: every async middleware mounted directly on a router owns its reject
92
92
  // The enumeration, kept as an executable assertion rather than prose so it cannot
93
93
  // rot: a NEW bare `async function require*` added later fails this test.
94
94
  // requireBuilder — the API gate. Guarded here (BV1.R109-follow-up).
95
- // requireBuilderPage \
96
- // requireGovernmentPage \
97
- // requireGovernmentManagePage \ all SIX delegate to runPageGate, guarded by BV1.R107.
98
- // requireNonXenosPage / (the R113 roles-tab gate; the /people curate
99
- // requireProjectCuratePage / gate; the ADR 0266 /board-room vote gate)
100
- // requireBoardVotePage /
95
+ // requireBuilderPage \
96
+ // requireGovernmentPage \ all FOUR delegate to runPageGate, guarded by
97
+ // requireProjectCuratePage / BV1.R107 (the /people curate gate; the
98
+ // requireBoardVotePage / ADR 0266 /board-room vote gate)
99
+ // Task 1003450 removed two more — requireNonXenosPage and the R113
100
+ // requireGovernmentManagePage — which had both ended up dispatched from no
101
+ // route at all; tests/hall_page_gate_map.mjs now fails if that recurs.
101
102
  // Anything else async and router-mounted must be added with its own try/catch.
102
103
  const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'auth.js'), 'utf8');
103
104
  const asyncMw = [...src.matchAll(/^async function (require\w+|allow\w+)\s*\(req, res, next\)/gm)].map((m) => m[1]);
104
- assert.deepEqual(asyncMw.sort(), ['requireBoardVotePage', 'requireBuilder', 'requireBuilderPage', 'requireGovernmentManagePage', 'requireGovernmentPage', 'requireNonXenosPage', 'requireProjectCuratePage'],
105
+ assert.deepEqual(asyncMw.sort(), ['requireBoardVotePage', 'requireBuilder', 'requireBuilderPage', 'requireGovernmentPage', 'requireProjectCuratePage'],
105
106
  'a new async router middleware appeared — give it a try/catch and add it here');
106
107
  // The page gates must actually delegate (that is WHY they are safe).
107
- for (const name of ['requireBuilderPage', 'requireGovernmentPage', 'requireGovernmentManagePage', 'requireNonXenosPage', 'requireProjectCuratePage', 'requireBoardVotePage']) {
108
+ for (const name of ['requireBuilderPage', 'requireGovernmentPage', 'requireProjectCuratePage', 'requireBoardVotePage']) {
108
109
  const fn = src.slice(src.indexOf(`async function ${name}`));
109
110
  assert.match(sliceFn(fn), /runPageGate\(/, `${name} must delegate to the guarded runPageGate`);
110
111
  }
@@ -50,14 +50,19 @@ const resolver = require('../modules/government/resolver.js');
50
50
  const wandering = require('../modules/builder-settings/wandering-prefs.js');
51
51
  const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
52
52
 
53
- const GATES = ['requireBuilderPage', 'requireNonXenosPage', 'requireGovernmentPage'];
53
+ const GATES = ['requireBuilderPage', 'requireGovernmentPage'];
54
54
  const LIVE_RANKS = ['xenos', 'thetes', 'metic', 'archon'];
55
55
 
56
56
  // ═══ the ORACLE — hand-written, from the rank tests R107 deleted ═════════════
57
57
  // requireBuilderPage admitted any session at all → every live rank
58
- // requireNonXenosPage admitted rank !== 'xenos' → thetes and up
59
58
  // requireGovernmentPage admitted rank === 'archon' → archon only …until ADR 0157
60
59
  //
60
+ // R107 migrated a THIRD gate, requireNonXenosPage (rank !== 'xenos' → thetes and
61
+ // up, via page.view.builder). Task 1002710 emptied it of routes and task 1003450
62
+ // deleted it; its rows are gone from the tables below rather than kept as an
63
+ // oracle for a function that no longer exists. The parity claim it carried is
64
+ // unaffected — it held right up to the deletion.
65
+ //
61
66
  // ADR 0157 (owner decision, 2026-08-05) lowered `page.view.government` archon→metic, so
62
67
  // the monitoring pages (/watch, /harbor, /gate) now open to the working rank. That gate
63
68
  // was spelled `requireArchonPage` through R107 and ADR 0157 — the name the rows below
@@ -70,7 +75,6 @@ const LIVE_RANKS = ['xenos', 'thetes', 'metic', 'archon'];
70
75
  // the gate matches its declared floor rather than merely "didn't change".
71
76
  const PRE_R107_AUDIENCE = {
72
77
  requireBuilderPage: { xenos: true, thetes: true, metic: true, archon: true },
73
- requireNonXenosPage: { xenos: false, thetes: true, metic: true, archon: true },
74
78
  requireGovernmentPage: { xenos: false, thetes: false, metic: true, archon: true },
75
79
  };
76
80
  // Where a SIGNED-IN builder who fails the gate is sent. Never the sign-in flow —
@@ -80,12 +84,10 @@ const PRE_R107_AUDIENCE = {
80
84
  // instead of silently dumping the builder at the home page.
81
85
  const DENIED_TO = {
82
86
  requireBuilderPage: '/builders',
83
- requireNonXenosPage: '/builders/not-ready',
84
87
  requireGovernmentPage: '/builders',
85
88
  };
86
89
  const GATE_PERMISSION = {
87
90
  requireBuilderPage: 'page.view.public',
88
- requireNonXenosPage: 'page.view.builder',
89
91
  requireGovernmentPage: 'page.view.government',
90
92
  };
91
93
  const deniedLoc = (gate, { unavailable = false } = {}) =>
@@ -93,7 +95,6 @@ const deniedLoc = (gate, { unavailable = false } = {}) =>
93
95
  // The equivalent rank floor, for the independent cross-check below.
94
96
  const GATE_FLOOR = {
95
97
  requireBuilderPage: 'xenos',
96
- requireNonXenosPage: 'thetes',
97
98
  // ADR 0157: `page.view.government` floor archon→metic.
98
99
  requireGovernmentPage: 'metic',
99
100
  };
@@ -264,9 +265,14 @@ test('B4: the page gates resolve per request — no memoised authority (ADR 0016
264
265
  test('B5: every permission a page gate names is a real catalog key', async () => {
265
266
  // Not an oracle — a sanity check that the gates did not migrate onto a typo,
266
267
  // which would silently deny everyone (fail-closed, but a bug all the same).
267
- for (const key of ['page.view.public', 'page.view.builder', 'page.view.government']) {
268
+ for (const key of ['page.view.public', 'page.view.government']) {
268
269
  assert.ok(catalog.byKey(key), `${key} must exist in the catalog`);
269
270
  }
271
+ // page.view.builder is deliberately NOT in that list any more: task 1003450
272
+ // deleted the only gate that named it. The atom is still seeded and attachable
273
+ // (catalog.js says so), so it is still a real key — it just is not a page
274
+ // gate's permission, and asserting it here would imply otherwise.
275
+ assert.ok(catalog.byKey('page.view.builder'), 'the atom survives its gate');
270
276
  });
271
277
 
272
278
  // ═══ PART C — the wandering CEILING (role metadata, not a permission) ════════
@@ -34,11 +34,13 @@ const SRC = fs.readFileSync(path.join(ROOT, 'src/bongos/serve-internal.js'), 'ut
34
34
  //
35
35
  // Audiences (the floors live in modules/government/catalog.js; the gate
36
36
  // functions in src/bongos/auth.js):
37
- // requireBuilderPage page.view.public any signed-in builder (xenos+)
37
+ // requireBuilderPage page.view.public any signed-in builder (xenos+)
38
38
  // requireGovernmentPage page.view.government metic+ (ADR 0157)
39
39
  // requireProjectCuratePage project.curate metic+
40
- // requireGovernmentManagePage government.manage archon
41
40
  // requireBoardVotePage board.vote.cast xenos+ (ADR 0266)
41
+ // That list is now CHECKED, not just written: "every page gate is dispatched"
42
+ // below reads the gate table out of auth.js and fails if a gate here guards no
43
+ // URL. Task 1003450 added it after finding two that did not.
42
44
  const ORACLE = {
43
45
  // Any signed-in builder — no rank floor.
44
46
  '/builders/work': 'requireBuilderPage',
@@ -60,10 +62,11 @@ const ORACLE = {
60
62
  '/builders/studio': 'requireBuilderPage',
61
63
  '/builders/ideas': 'requireBuilderPage',
62
64
  '/builders/collab': 'requireBuilderPage',
63
- // task 1002710: the orientation reading rooms moved DOWN to this tier from
64
- // requireNonXenosPage (thetes+). They are the onboarding documents, so gating
65
- // them above xenos turned away the exact reader they were written for. The
66
- // sign-in requirement is unchanged — only the RANK floor is gone.
65
+ // task 1002710: the orientation reading rooms moved DOWN to this tier from a
66
+ // thetes+ gate. They are the onboarding documents, so gating them above xenos
67
+ // turned away the exact reader they were written for. The sign-in requirement
68
+ // is unchanged — only the RANK floor is gone. (Task 1003450 then deleted the
69
+ // gate those two rows left behind; see GONE_GATES below.)
67
70
  '/builders/primer': 'requireBuilderPage',
68
71
  '/builders/primer.md': 'requireBuilderPage',
69
72
  '/builders/diagrams': 'requireBuilderPage',
@@ -118,7 +121,6 @@ const ORACLE = {
118
121
  '/builders/ranks': null,
119
122
  '/builders/drachmae': null,
120
123
  '/builders/profile': null,
121
- '/builders/not-ready': null,
122
124
 
123
125
  // NOT deliberate — recorded because it is true, and filed as idea 1000907
124
126
  // (task 1003448). The detail pages are addressed by a second segment, and
@@ -137,6 +139,15 @@ const ORACLE = {
137
139
  // from the map" is indistinguishable from the oversight this file exists to
138
140
  // catch. GONE_PAGES below keeps the row honest if the file ever returns.
139
141
  '/builders/my-projects': null,
142
+
143
+ // Gone too, and for a reason worth keeping in front of the next reader: this
144
+ // was a bespoke refusal PAGE ("not yet — ship three tasks to graduate"), the
145
+ // deniedTo of the thetes+ gate. Task 1002613 replaced that whole idea with a
146
+ // banner the shell renders from ?denied=<permission> on whatever page the
147
+ // redirect lands on — which names the permission actually missing instead of
148
+ // assuming one tier's remedy. Once task 1002710 emptied the gate, nothing
149
+ // linked here or redirected here; task 1003450 removed page and gate together.
150
+ '/builders/not-ready': null,
140
151
  };
141
152
 
142
153
  // ─── reconstruct the URL→gate map from the source ───────────────────────────
@@ -250,6 +261,8 @@ const GONE_PAGES = {
250
261
  // redirect. tests/hall_settings_world.mjs pins the deletion itself; this row
251
262
  // pins what the deletion means for the gate map.
252
263
  '/builders/my-projects': 'task 1003308 — the hub owns "my projects"',
264
+ // Task 1003450 — the shell's ?denied= banner replaced the bespoke refusal page.
265
+ '/builders/not-ready': 'task 1003450 — the shell renders the refusal from ?denied=',
253
266
  };
254
267
 
255
268
  test('a row that is null because the page left the hall serves no page', () => {
@@ -261,12 +274,72 @@ test('a row that is null because the page left the hall serves no page', () => {
261
274
  }
262
275
  });
263
276
 
264
- test('no gated page is left matching only a REMOVED gate function', () => {
265
- // requireNonXenosPage still EXISTS in auth.js (page.view.builder is a live
266
- // catalog atom an Archon can attach to a custom role) but task 1002710 left it
267
- // guarding nothing. If someone re-points a URL at it, that is a deliberate act
268
- // that should update this oracle not something to happen silently.
277
+ // ─── the gate table itself: nothing exported that guards nothing ────────────
278
+ //
279
+ // The previous version of this block asserted, by name, that ONE gate
280
+ // (requireNonXenosPage) guarded no URL recording the dead code instead of
281
+ // removing it. That reads as coverage and is the opposite: it pinned the defect
282
+ // in place, and it could not see requireGovernmentManagePage, which had gone the
283
+ // same way when R16 widened /government's shell to the metic floor. Both gates
284
+ // are gone (task 1003450). What replaces the row is the general rule, which
285
+ // fails on the NEXT one without anybody having to think of its name.
286
+ //
287
+ // Why it matters beyond tidiness: an exported gate with no route is a loaded
288
+ // gun. It still resolves a permission and still redirects, so wiring it is one
289
+ // line — and the reviewer of that line sees a function that looks maintained.
290
+ // requireNonXenosPage would have re-imposed a rank floor the owner had
291
+ // deliberately removed (task 1002710), and its deniedTo pointed at a page that
292
+ // by then said the wrong thing.
293
+ const AUTH_SRC = fs.readFileSync(path.join(ROOT, 'src/bongos/auth.js'), 'utf8');
294
+
295
+ // The keys of the PAGE_GATES table, read statically — the same posture as the
296
+ // dispatch parse above (importing auth.js pulls the whole app in).
297
+ function readPageGateNames(src) {
298
+ const table = /const PAGE_GATES = Object\.freeze\(\{([\s\S]*?)\n\}\);/.exec(src);
299
+ assert.ok(table, 'PAGE_GATES is still a `const PAGE_GATES = Object.freeze({…});` literal');
300
+ // Entry keys sit at exactly one indent level; `permission:` / `deniedTo:` /
301
+ // `alsoAdmits:` are nested deeper, so the two-space anchor keeps them out.
302
+ return [...table[1].matchAll(/^ {2}(require\w+):/gm)].map((m) => m[1]);
303
+ }
304
+
305
+ test('every page gate in the table is dispatched by at least one hall URL', () => {
306
+ const declared = readPageGateNames(AUTH_SRC);
307
+ assert.ok(declared.length >= 4, 'the gate table was found — an empty parse must not pass silently');
269
308
  const wired = new Set(DISPATCH.map((d) => d.gate));
270
- assert.ok(!wired.has('requireNonXenosPage'),
271
- 'requireNonXenosPage guards no hall URL since task 1002710 — update the ORACLE if that changes');
309
+ for (const gate of declared) {
310
+ assert.ok(wired.has(gate),
311
+ `${gate} is declared in PAGE_GATES and dispatched from no route. Either wire it to a URL `
312
+ + '(and add that URL to the ORACLE above), or delete the gate — an exported gate that '
313
+ + 'guards nothing is a rank floor waiting to be re-imposed by accident.');
314
+ }
315
+ });
316
+
317
+ test('every gate a URL dispatches is declared in the gate table', () => {
318
+ // The other direction: gdsAuth.<name> in serve-internal.js is resolved at
319
+ // runtime, so a typo or a rename is an undefined-is-not-a-function on the
320
+ // first request to that page rather than a boot failure.
321
+ const declared = new Set(readPageGateNames(AUTH_SRC));
322
+ for (const { gate } of DISPATCH) {
323
+ assert.ok(declared.has(gate), `serve-internal.js dispatches gdsAuth.${gate}, which PAGE_GATES does not declare`);
324
+ }
325
+ });
326
+
327
+ test("every gate's deniedTo is a hall page that is actually served", () => {
328
+ // The failure this catches is the one task 1003450 cleaned up, in its most
329
+ // dangerous form: deleting a page that a live gate still redirects to. The
330
+ // refused visitor would land on a 404 with no way back and no explanation —
331
+ // strictly worse than the 403 the redirect exists to avoid.
332
+ const dests = [...AUTH_SRC.matchAll(/deniedTo:\s*'([^']+)'/g)].map((m) => m[1]);
333
+ assert.ok(dests.length, 'the deniedTo destinations were found');
334
+ for (const dest of dests) {
335
+ assert.ok(dest.startsWith('/builders'), `${dest} must be a hall URL`);
336
+ const slug = dest.replace(/^\/builders\/?/, '');
337
+ // The oracle spells the hall index with its trailing slash; every other row
338
+ // is written without one.
339
+ assert.equal(ORACLE[slug ? dest : '/builders/'], null,
340
+ `${dest} is a deniedTo, so it must be UNGATED — redirecting a refused visitor at a gated page loops`);
341
+ if (!slug) continue; // /builders itself is the hall index, served from index.html
342
+ assert.ok(fs.existsSync(path.join(HALL_PAGES_DIR, `${slug}.html`)),
343
+ `${dest} is a deniedTo destination but no page is served there — a refused visitor gets a 404`);
344
+ }
272
345
  });
@@ -1,5 +1,5 @@
1
1
  // tests/hall_settings_world.mjs — the PANEL family in the chrome world
2
- // (task 1003308, goal 1000081): settings, profile, pair, not-ready.
2
+ // (task 1003308, goal 1000081): settings, profile, pair.
3
3
  //
4
4
  // tests/hall_tokens.mjs pins the TIER these pages spend; this pins how they
5
5
  // spend it. Everything here is a fact that is silent in a browser: the page
@@ -33,7 +33,7 @@
33
33
  // node scripts/hall-preview/server.js --port 4621 --fixture-me
34
34
  // node modules/ui-design/kit/render.js --states modules/hall-ui/public/settings.states.json \
35
35
  // --base-in http://builders.localhost:4621 --base-out http://builders.localhost:4621
36
- // (and profile / pair / not-ready .states.json the same way)
36
+ // (and profile / pair .states.json the same way)
37
37
 
38
38
  import { test } from 'node:test';
39
39
  import assert from 'node:assert/strict';
@@ -46,7 +46,10 @@ const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
46
46
  const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
47
47
  const stripComments = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
48
48
 
49
- const PAGES = ['settings', 'profile', 'pair', 'not-ready'];
49
+ // not-ready was the fourth until task 1003450 deleted the page — see
50
+ // tests/hall_page_gate_map.mjs (GONE_PAGES) for why, and for the row that stops
51
+ // it coming back ungated. The archetype is unaffected: three pages still spend it.
52
+ const PAGES = ['settings', 'profile', 'pair'];
50
53
  const SHEETS = ['panel.css', 'settings.css', 'pair.css', 'profile.css'];
51
54
 
52
55
  const panelCss = read('panel.css');
@@ -166,9 +166,12 @@ await t('the OTHER page gates are untouched — their seam is never consulted at
166
166
  'and its denial is unchanged, down to the permission it names');
167
167
  const { nexted: pub } = await runGate('requireBuilderPage', OWNER, spy);
168
168
  assert.ok(pub, 'the public hall page still admits every signed-in builder, exactly as before');
169
- const { res: nx } = await runGate('requireNonXenosPage', OWNER, spy);
170
- assert.deepEqual(nx._redirect, { code: 302, loc: '/builders/not-ready?denied=page.view.builder' },
171
- 'and the orientation rooms still send a xenos to the friendly notice');
169
+ // A third gate stood here requireNonXenosPage, refusing a xenos to
170
+ // /builders/not-ready. Task 1003450 deleted it and that page together; the two
171
+ // above carry the same claim (a gate declaring no alsoAdmits never touches the
172
+ // seam), and the board-room gate below is the wider third case.
173
+ const { nexted: vote } = await runGate('requireBoardVotePage', OWNER, spy);
174
+ assert.ok(vote, 'board.vote.cast floors at xenos on purpose (ADR 0266) — admitted without the seam');
172
175
  });
173
176
 
174
177
  await t('only ONE gate declares a second door (a widening must be opted into, one gate at a time)', () => {
@@ -27,7 +27,7 @@ import os from 'node:os';
27
27
  import path from 'node:path';
28
28
 
29
29
  const require = createRequire(import.meta.url);
30
- const { worktreeNameFor, needsJunction, removalPlan, nodeModulesState } = require('../scripts/gds/worktree.js');
30
+ const { worktreeNameFor, needsJunction, removalPlan, nodeModulesState, shadowingNodeModules } = require('../scripts/gds/worktree.js');
31
31
 
32
32
  let passed = 0;
33
33
  let failed = 0;
@@ -140,6 +140,55 @@ test('a REAL node_modules dir is left for git to refuse — we never rm it', ()
140
140
  eqArr(removalPlan({ nodeModulesState: 'real-dir' }), ['git-worktree-remove']);
141
141
  });
142
142
 
143
+ // ---- shadowingNodeModules (task 1003627) ----------------------------------
144
+ // An inside-repo worktree gets NO junction by design — node walks up to the main
145
+ // checkout. But walk-up stops at the FIRST node_modules, so a stray one in
146
+ // between captures every inside-repo worktree at once, and `require` still
147
+ // SUCCEEDS from the wrong place. Two agents read that as "worktree.js add failed
148
+ // to make a junction" on 2026-09-04; there was no junction to make.
149
+ const R = 'C:/repo';
150
+ const WT = 'C:/repo/.claude/worktrees/task-1';
151
+ const at = (...present) => {
152
+ const set = new Set(present.map((p) => path.resolve(p)));
153
+ return (p) => set.has(path.resolve(p));
154
+ };
155
+
156
+ test('names a stray node_modules one level above the worktree', () => {
157
+ const hit = shadowingNodeModules({
158
+ wtPath: WT, repoRoot: R, existsFn: at('C:/repo/.claude/worktrees/node_modules'),
159
+ });
160
+ eq(path.resolve(hit), path.resolve('C:/repo/.claude/worktrees/node_modules'));
161
+ });
162
+
163
+ test('names a stray further up too (any level BELOW the repo root shadows it)', () => {
164
+ const hit = shadowingNodeModules({ wtPath: WT, repoRoot: R, existsFn: at('C:/repo/.claude/node_modules') });
165
+ eq(path.resolve(hit), path.resolve('C:/repo/.claude/node_modules'));
166
+ });
167
+
168
+ test('the repo root\'s OWN node_modules is not a stray — that is the target', () => {
169
+ // The whole point of walk-up is to reach this one; flagging it would make the
170
+ // warning fire on every healthy worktree, which is how a warning becomes noise.
171
+ eq(shadowingNodeModules({ wtPath: WT, repoRoot: R, existsFn: at('C:/repo/node_modules') }), null);
172
+ });
173
+
174
+ test('no stray anywhere → null', () => {
175
+ eq(shadowingNodeModules({ wtPath: WT, repoRoot: R, existsFn: () => false }), null);
176
+ });
177
+
178
+ test('an OUTSIDE-repo worktree is not checked — it gets a junction instead', () => {
179
+ eq(shadowingNodeModules({ wtPath: 'C:/tmp/wt-9', repoRoot: R, existsFn: () => true }), null);
180
+ });
181
+
182
+ test('the nearest stray wins, matching node\'s own resolution order', () => {
183
+ const hit = shadowingNodeModules({
184
+ wtPath: WT,
185
+ repoRoot: R,
186
+ existsFn: at('C:/repo/.claude/worktrees/node_modules', 'C:/repo/.claude/node_modules'),
187
+ });
188
+ eq(path.resolve(hit), path.resolve('C:/repo/.claude/worktrees/node_modules'),
189
+ 'node stops at the first node_modules it meets walking up');
190
+ });
191
+
143
192
  console.log('');
144
193
  console.log(`worktree_helper: ${passed} passed, ${failed} failed`);
145
194
  process.exit(failed > 0 ? 1 : 0);
@@ -1,53 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Not yet available — {{worldName}}</title>
7
- <meta name="description" content="This page unlocks as you advance. Ship your first three tasks to open the rest of the builder hub.">
8
- <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
- <link rel="preconnect" href="https://fonts.googleapis.com">
10
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
12
- <!-- The PANEL archetype (task 1003308): this page is its sign-in gate shape. -->
13
- <link rel="stylesheet" href="/builders/panel.css">
14
- </head>
15
- <body data-page="not-ready" data-page-title="Locked">
16
-
17
- <div class="app">
18
- <aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
19
- <div class="app-scrim" id="app-scrim" hidden></div>
20
- <div class="app-body">
21
- <header class="app-topbar" id="app-topbar"></header>
22
- <div class="app-content">
23
- <!-- The page's ONE h1 (one-header-main-h1), in the family's voice. -->
24
- <div class="page-head page-head--room page-head--panel">
25
- <h1 id="not-ready-h1" class="page-head__h">not yet</h1>
26
- <p class="page-head__sub">This page unlocks as you advance.</p>
27
- </div>
28
- <main id="hall-main">
29
- <section class="scroll gate" aria-labelledby="not-ready-h">
30
- <h2 id="not-ready-h" class="gate__h">Not yet available</h2>
31
- <p class="gate__lede">
32
- Xenos builders start with a focused set of pages — ship your first
33
- <strong>three tasks</strong> to graduate and open the rest of
34
- {{worldName}}'s builder hub.
35
- </p>
36
- <div class="row-end">
37
- <a class="pbtn pbtn--save" href="/ranks">See how ranks work</a>
38
- <a class="pbtn pbtn--quiet" href="/">Back to home</a>
39
- </div>
40
- </section>
41
- </main>
42
- <footer class="app-foot" id="app-foot"></footer>
43
- </div>
44
- </div>
45
- </div>
46
-
47
- <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
48
-
49
- <script src="/builders/shell.js"></script>
50
- <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
51
- <script src="/builders/palette.js" defer></script>
52
- </body>
53
- </html>
@@ -1,14 +0,0 @@
1
- {
2
- "_": "The NOT-READY page through modules/ui-design/kit/render.js (task 1003308) — the sign-in gate shape of the panel family, static markup with no feed. Renders under the kit's own stub and under the harness alike.",
3
- "page": "/not-ready",
4
- "surface": "hall-ui",
5
- "stub": { "prefix": "/builders/" },
6
- "modeQuery": false,
7
- "ignoreRequests": ["/api/(gds|bongos)/"],
8
- "ignoreConsole": ["/api/(gds|bongos)/", "\b401\b", "\b404\b", "Unauthorized", "Not Found", "load failed"],
9
- "states": {
10
- "locked": { "auth": true, "actions": [["wait", 400]], "expect": { "visible": ["#not-ready-h1", "#not-ready-h"] } },
11
- "out": { "auth": false, "actions": [["wait", 400]], "expect": { "visible": ["#not-ready-h1"] } }
12
- },
13
- "reducedMotion": { "auth": true, "actions": [["wait", 400]] }
14
- }