@bongos/core 1.20.47 → 1.20.48
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 +81 -56
- package/docs/copy-inventory.md +82 -80
- package/docs/copy-registry.json +109 -91
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +5 -3
- package/docs/page-readings.json +242 -243
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/hall-render.js +3 -0
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +82 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +36 -0
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work-founding.states.json +39 -0
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/founding-mode.md +31 -0
- package/modules/onboarding/founding-mode.js +185 -0
- package/modules/onboarding/port.js +7 -0
- package/modules/ui-design/kit/fixtures/me-founding.json +24 -0
- package/modules/ui-design/kit/serve.js +3 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/bongos/routes/me.js +18 -1
- package/src/module-api.js +1 -1
- package/tests/founding_mode.mjs +294 -0
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/onboarding/founding-mode.js — PROJECT FOUNDING MODE: is this project still
|
|
4
|
+
// being founded, and which stage of genesis is it on? (task 1004422 / BV2.PS03, goal
|
|
5
|
+
// 1000121; design of record docs/design/project-startup-direction.md.)
|
|
6
|
+
//
|
|
7
|
+
// THE BLEND RULE THIS FILE SERVES (owner, 2026-09-30). Genesis is a MODE the existing
|
|
8
|
+
// builders hall wears, never a second hall. So this file answers ONE question for the
|
|
9
|
+
// hall shell — founding or not, and at which stage — and the shell stamps the answer on
|
|
10
|
+
// the document the way it stamps social mode (`html[data-social]`). Nothing here renders
|
|
11
|
+
// anything, and a project that is not founding gets `null`, which the shell reads as OFF:
|
|
12
|
+
// the hall then renders exactly as it does today.
|
|
13
|
+
//
|
|
14
|
+
// WHY THE ONBOARDING MODULE. Founding is the FOUNDER-side counterpart of the joiner
|
|
15
|
+
// state machine this module already owns (onboarding-state.js graduates a newcomer; this
|
|
16
|
+
// graduates a project). It lives in project_settings (migration core_231) through the
|
|
17
|
+
// doorway's one accessor, like mingle.enabled, rather than a table of its own — one row
|
|
18
|
+
// per project is the whole state, and "do not fork a second settings store" is that
|
|
19
|
+
// accessor's own rule.
|
|
20
|
+
//
|
|
21
|
+
// FAIL POSTURE: CLOSED, i.e. OFF. An unreadable, absent, or malformed state is NOT
|
|
22
|
+
// founding. The failure this guards against is the owner's no-hijack condition: a hall
|
|
23
|
+
// wrongly stuck in founding mode would dim rooms that are really open. A hall wrongly
|
|
24
|
+
// NOT in founding mode only loses a banner. So every doubtful read answers null.
|
|
25
|
+
|
|
26
|
+
// The genesis stages, in order. 'decide' is the pre-stage the founder plans in; the
|
|
27
|
+
// five after it are the stages proper (spec C4). The order IS the data: a room wakes
|
|
28
|
+
// when the stage that wakes it has been left behind.
|
|
29
|
+
const STAGES = Object.freeze(['decide', 'scope', 'strategize', 'charter', 'legalize', 'legislation']);
|
|
30
|
+
|
|
31
|
+
// Which hall rooms each finished stage wakes (the direction doc's "How rooms wake"
|
|
32
|
+
// table). Keyed by the shell's NAV item ids, so the client applies it without a second
|
|
33
|
+
// mapping of its own — a surface that re-derived this could disagree with the server.
|
|
34
|
+
// 'community' wakes only when the planet is ALIVE, which is when founding mode switches
|
|
35
|
+
// off entirely, so during founding it is always asleep.
|
|
36
|
+
const WAKES = Object.freeze({
|
|
37
|
+
scope: Object.freeze(['roadmap', 'goals', 'goal-map']),
|
|
38
|
+
strategize: Object.freeze(['work']),
|
|
39
|
+
charter: Object.freeze(['board-room']),
|
|
40
|
+
legalize: Object.freeze(['economy']),
|
|
41
|
+
legislation: Object.freeze(['ranks']),
|
|
42
|
+
});
|
|
43
|
+
const WAKES_WHEN_ALIVE = Object.freeze(['collab', 'roster']);
|
|
44
|
+
|
|
45
|
+
// The project_settings keys. Namespaced 'founding.' like 'mingle.' and 'attention.'.
|
|
46
|
+
const KEYS = Object.freeze({
|
|
47
|
+
ACTIVE: 'founding.active',
|
|
48
|
+
STAGE: 'founding.stage',
|
|
49
|
+
STARTED_AT: 'founding.started_at',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// The only 'on' spelling accepted, mirroring mingle's consent-gate parse: an explicit,
|
|
53
|
+
// recognised yes. Anything else is off.
|
|
54
|
+
function parseActive(raw) {
|
|
55
|
+
if (raw == null) return false;
|
|
56
|
+
return ['true', '1', 'on', 'yes'].includes(String(raw).trim().toLowerCase());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// asleepRooms(stage) — PURE. For a known stage, { navId: wakingStage } for every room
|
|
60
|
+
// still asleep. A room is awake once its waking stage is BEHIND the current one.
|
|
61
|
+
function asleepRooms(stage) {
|
|
62
|
+
const at = STAGES.indexOf(stage);
|
|
63
|
+
if (at < 0) return null;
|
|
64
|
+
const asleep = {};
|
|
65
|
+
for (const [wakingStage, ids] of Object.entries(WAKES)) {
|
|
66
|
+
if (STAGES.indexOf(wakingStage) >= at) for (const id of ids) asleep[id] = wakingStage;
|
|
67
|
+
}
|
|
68
|
+
for (const id of WAKES_WHEN_ALIVE) asleep[id] = 'alive';
|
|
69
|
+
return asleep;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// dayOf(startedAt, now) — PURE. Day 1 is the founding day. null when unreadable, so a
|
|
73
|
+
// bad timestamp drops the day from the band rather than printing "day NaN".
|
|
74
|
+
function dayOf(startedAt, now = new Date()) {
|
|
75
|
+
if (!startedAt) return null;
|
|
76
|
+
const t = new Date(startedAt).getTime();
|
|
77
|
+
if (!Number.isFinite(t)) return null;
|
|
78
|
+
const days = Math.floor((now.getTime() - t) / 86400000) + 1;
|
|
79
|
+
return days >= 1 ? days : 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// shapeFoundingMode(rows, now) — PURE. rows = { active, stage, startedAt } raw strings
|
|
83
|
+
// (or nulls). Returns the /me payload, or null for "not founding" in any form.
|
|
84
|
+
function shapeFoundingMode(rows, now = new Date()) {
|
|
85
|
+
if (!rows || !parseActive(rows.active)) return null;
|
|
86
|
+
const stage = rows.stage == null ? 'decide' : String(rows.stage).trim().toLowerCase();
|
|
87
|
+
const asleep = asleepRooms(stage);
|
|
88
|
+
if (!asleep) return null; // an unknown stage is not a stage — fail closed (OFF)
|
|
89
|
+
const index = STAGES.indexOf(stage);
|
|
90
|
+
return {
|
|
91
|
+
active: true,
|
|
92
|
+
stage,
|
|
93
|
+
// 0 while deciding, then 1..5 through the stages proper.
|
|
94
|
+
stage_number: index,
|
|
95
|
+
stage_count: STAGES.length - 1,
|
|
96
|
+
day: dayOf(rows.startedAt, now),
|
|
97
|
+
asleep,
|
|
98
|
+
// Who closing the current stage is waiting on (spec D7) — display names, at most
|
|
99
|
+
// five, from the government module's genesis-stage sitting (task 1004429). null
|
|
100
|
+
// when the stage is not sitting, nobody is left to vote, or the read is missing.
|
|
101
|
+
waiting_on: Array.isArray(rows.waitingOn) && rows.waitingOn.length
|
|
102
|
+
? rows.waitingOn.slice(0, 5).map((n) => String(n))
|
|
103
|
+
: null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// foundingModeFor(deps) — the /me read. Resolves the three rows through the doorway's
|
|
108
|
+
// project-settings accessor (injectable for tests). Never throws: any read failure is
|
|
109
|
+
// "not founding".
|
|
110
|
+
// Three primary-key reads in parallel: the doorway's project-settings accessor is
|
|
111
|
+
// single-key by design (one accessor, one table), and a founding project is a short
|
|
112
|
+
// window of a project's life, so a batched read is not worth a second accessor.
|
|
113
|
+
async function foundingModeFor(deps = {}) {
|
|
114
|
+
const get = deps.getSetting || require('../../src/module-api').projectSettings.get;
|
|
115
|
+
let shaped;
|
|
116
|
+
try {
|
|
117
|
+
const [active, stage, startedAt] = await Promise.all([
|
|
118
|
+
get(KEYS.ACTIVE), get(KEYS.STAGE), get(KEYS.STARTED_AT),
|
|
119
|
+
]);
|
|
120
|
+
shaped = shapeFoundingMode({
|
|
121
|
+
active: active && active.value,
|
|
122
|
+
stage: stage && stage.value,
|
|
123
|
+
startedAt: startedAt && startedAt.value,
|
|
124
|
+
}, deps.now || new Date());
|
|
125
|
+
} catch (_) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
if (!shaped) return null;
|
|
129
|
+
// The waiting-on names come from the government module's genesis-stage read
|
|
130
|
+
// (genesisStageStatus on its port, task 1004429: one entry per stage, each with
|
|
131
|
+
// a state and, while sitting, the members still to vote under the item's own
|
|
132
|
+
// snapshot). Optional and fail-open: a missing port or a failed read leaves the
|
|
133
|
+
// band without the names, never without the mode.
|
|
134
|
+
try {
|
|
135
|
+
const waitingOnFor = deps.waitingOnFor || waitingOnFromGovernment(
|
|
136
|
+
deps.government !== undefined ? deps.government : require('../../src/module-api').resolveOptional('government'),
|
|
137
|
+
);
|
|
138
|
+
if (waitingOnFor) {
|
|
139
|
+
const names = await waitingOnFor(shaped.stage);
|
|
140
|
+
if (Array.isArray(names) && names.length) shaped.waiting_on = names.slice(0, 5).map((n) => String(n));
|
|
141
|
+
}
|
|
142
|
+
} catch (_) { /* fail open: the band shows the stage without the names */ }
|
|
143
|
+
return shaped;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// waitingOnFromGovernment(gov) — a stage -> [display name] reader over the government
|
|
147
|
+
// port's genesisStageStatus(), or null when the port does not offer it. Only a stage
|
|
148
|
+
// that is SITTING waits on anyone; not_put, passed and returned wait on nobody.
|
|
149
|
+
function waitingOnFromGovernment(gov) {
|
|
150
|
+
if (!gov || typeof gov.genesisStageStatus !== 'function') return null;
|
|
151
|
+
return async (stage) => {
|
|
152
|
+
const status = await gov.genesisStageStatus();
|
|
153
|
+
const row = status && Array.isArray(status.stages) ? status.stages.find((s) => s.key === stage) : null;
|
|
154
|
+
if (!row || row.state !== 'sitting' || !Array.isArray(row.waiting_on)) return [];
|
|
155
|
+
return row.waiting_on.map((m) => m.display_name || m.github_login).filter(Boolean);
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// setFoundingMode({ active, stage }, builderId, deps) — the write the genesis home
|
|
160
|
+
// (task 1004423) drives: start founding on a new project, move the stage, and switch
|
|
161
|
+
// founding off when the planet comes alive. Refuses an unknown stage rather than storing
|
|
162
|
+
// a value the read would then treat as OFF. Starting stamps started_at once.
|
|
163
|
+
async function setFoundingMode({ active, stage } = {}, builderId = null, deps = {}) {
|
|
164
|
+
const settings = require('../../src/module-api').projectSettings;
|
|
165
|
+
const get = deps.getSetting || settings.get;
|
|
166
|
+
const set = deps.setSetting || settings.set;
|
|
167
|
+
if (stage != null && !STAGES.includes(stage)) {
|
|
168
|
+
const err = new Error(`unknown founding stage '${stage}'`);
|
|
169
|
+
err.code = 'bad_founding_stage';
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
if (active === true) {
|
|
173
|
+
const started = await get(KEYS.STARTED_AT);
|
|
174
|
+
if (!started || !started.value) await set(KEYS.STARTED_AT, (deps.now || new Date()).toISOString(), builderId);
|
|
175
|
+
}
|
|
176
|
+
if (typeof active === 'boolean') await set(KEYS.ACTIVE, active ? 'true' : 'false', builderId);
|
|
177
|
+
if (stage != null) await set(KEYS.STAGE, stage, builderId);
|
|
178
|
+
return foundingModeFor({ getSetting: get, now: deps.now });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
STAGES, WAKES, WAKES_WHEN_ALIVE, KEYS,
|
|
183
|
+
parseActive, asleepRooms, dayOf, shapeFoundingMode, waitingOnFromGovernment,
|
|
184
|
+
foundingModeFor, setFoundingMode,
|
|
185
|
+
};
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
|
|
41
41
|
const onboardingState = require('./onboarding-state');
|
|
42
42
|
const newcomerRestock = require('./newcomer-restock');
|
|
43
|
+
const foundingMode = require('./founding-mode');
|
|
43
44
|
|
|
44
45
|
module.exports = {
|
|
45
46
|
// --- the newcomer onboarding state machine (onboarding-state.js) ---------
|
|
@@ -53,6 +54,12 @@ module.exports = {
|
|
|
53
54
|
markStage: (...a) => onboardingState.markStage(...a),
|
|
54
55
|
broadcastGraduation: (...a) => onboardingState.broadcastGraduation(...a),
|
|
55
56
|
|
|
57
|
+
// --- project founding mode (founding-mode.js, task 1004422) ---------------
|
|
58
|
+
// me.js reads it for the hall shell (null = not founding = the hall as today);
|
|
59
|
+
// the genesis home (task 1004423) drives the write.
|
|
60
|
+
foundingModeFor: (...a) => foundingMode.foundingModeFor(...a),
|
|
61
|
+
setFoundingMode: (...a) => foundingMode.setFoundingMode(...a),
|
|
62
|
+
|
|
56
63
|
// --- the evergreen newcomer-chore restock (newcomer-restock.js) ----------
|
|
57
64
|
// restock the just-claimed chore's shelf (claims.js / tasks.js); the Archon
|
|
58
65
|
// backstop tops every chore up (tasks.js). SOURCE_TAG is the marker the claim
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"builder": {
|
|
3
|
+
"id": "1",
|
|
4
|
+
"github_login": "builder",
|
|
5
|
+
"display_name": "A Builder",
|
|
6
|
+
"avatar_url": "/__kit/avatar.svg",
|
|
7
|
+
"rank": "archon"
|
|
8
|
+
},
|
|
9
|
+
"founding_mode": {
|
|
10
|
+
"active": true,
|
|
11
|
+
"stage": "strategize",
|
|
12
|
+
"stage_number": 2,
|
|
13
|
+
"stage_count": 5,
|
|
14
|
+
"day": 9,
|
|
15
|
+
"asleep": {
|
|
16
|
+
"work": "strategize",
|
|
17
|
+
"board-room": "charter",
|
|
18
|
+
"economy": "legalize",
|
|
19
|
+
"ranks": "legislation",
|
|
20
|
+
"collab": "alive",
|
|
21
|
+
"roster": "alive"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -277,7 +277,9 @@ function fileUnder(base, rel) {
|
|
|
277
277
|
// is absolutised to this origin per request.
|
|
278
278
|
const AVATAR_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><circle cx="32" cy="32" r="32" fill="#888"/><circle cx="32" cy="26" r="11" fill="#ddd"/><path d="M12 56c3-12 11-17 20-17s17 5 20 17" fill="#ddd"/></svg>';
|
|
279
279
|
function me(origin) {
|
|
280
|
-
|
|
280
|
+
// stub.fixtures may re-point the identity too (task 1004422: the founding-mode render) —
|
|
281
|
+
// the same ALIAS every other fixture honours, so a variant states file needs no stub code.
|
|
282
|
+
const m = JSON.parse(fs.readFileSync(path.join(FIXTURES, (ALIAS.me || 'me') + '.json'), 'utf8'));
|
|
281
283
|
if (m.builder && typeof m.builder.avatar_url === 'string' && m.builder.avatar_url.startsWith('/')) m.builder.avatar_url = origin + m.builder.avatar_url;
|
|
282
284
|
return m;
|
|
283
285
|
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.48",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.48",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.48",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -8395,5 +8395,11 @@
|
|
|
8395
8395
|
"id": "1003141",
|
|
8396
8396
|
"text": "The Goals page now has the same bulk actions as the Tasks board: claim several tasks, release your own, or copy a prompt to work them in parallel or in order. Both pages now share one toolbar, so they stay in step."
|
|
8397
8397
|
}
|
|
8398
|
+
],
|
|
8399
|
+
"1.20.48": [
|
|
8400
|
+
{
|
|
8401
|
+
"id": "1004422",
|
|
8402
|
+
"text": "While a new project is being founded, its builders hall now shows a clear \"project founding mode\" band at the top and dims the rooms that open later. A hall that isn't being founded stays exactly as it was."
|
|
8403
|
+
}
|
|
8398
8404
|
]
|
|
8399
8405
|
}
|
package/src/bongos/routes/me.js
CHANGED
|
@@ -187,7 +187,7 @@ module.exports = function buildMeRouter() {
|
|
|
187
187
|
recentGrades, builderAvg, onboardingRow, shipCount, recentKarma, karma30d, discordLink, cliAuthed,
|
|
188
188
|
behaviorPrefs, geminiKeyRow, needsRebaseTasks, foundingBuilder, helpRequestCount,
|
|
189
189
|
taskRecommendationCount, boardVotesPending, socialMode, mingleIntroductionCount,
|
|
190
|
-
navPermissions,
|
|
190
|
+
navPermissions, foundingMode,
|
|
191
191
|
] = await Promise.all([
|
|
192
192
|
db.getBuilderById(req.builder.id),
|
|
193
193
|
lc().getActiveClaimsForBuilder(req.builder.id),
|
|
@@ -308,6 +308,16 @@ module.exports = function buildMeRouter() {
|
|
|
308
308
|
// Same fan-out slot as the reads above, but it fails CLOSED rather than
|
|
309
309
|
// open — null hides the link. See navPermissionsFor.
|
|
310
310
|
navPermissionsFor(req.builder.id),
|
|
311
|
+
// task 1004422 (BV2.PS03): PROJECT FOUNDING MODE — is this project still being
|
|
312
|
+
// founded, and at which genesis stage. Same fan-out slot and posture as social
|
|
313
|
+
// mode: null (not founding, or unreadable) is OFF, and the hall then renders
|
|
314
|
+
// exactly as it does today, which is the owner's no-hijack condition.
|
|
315
|
+
(async () => {
|
|
316
|
+
try {
|
|
317
|
+
if (!onboarding || !onboarding.foundingModeFor) return null;
|
|
318
|
+
return await onboarding.foundingModeFor();
|
|
319
|
+
} catch (_) { return null; }
|
|
320
|
+
})(),
|
|
311
321
|
]);
|
|
312
322
|
// V3.R84 (#302), criterion C3: the builder's month-to-date spend vs their
|
|
313
323
|
// monthly budget. Surfaced here so the CLI (session-start ping) and the hall
|
|
@@ -378,6 +388,13 @@ module.exports = function buildMeRouter() {
|
|
|
378
388
|
// when lifecycle is disabled or the read failed, and the shell treats null
|
|
379
389
|
// as OFF — a mode that cannot be read is not on.
|
|
380
390
|
social: socialMode,
|
|
391
|
+
// task 1004422 (BV2.PS03, goal 1000121): PROJECT FOUNDING MODE. Genesis is a
|
|
392
|
+
// mode the EXISTING hall wears (the blend rule, docs/design/project-startup-
|
|
393
|
+
// direction.md), so it rides /me exactly like social: the shell stamps
|
|
394
|
+
// html[data-founding], draws the founding band and dims the rooms still asleep.
|
|
395
|
+
// Shape: { active, stage, stage_number, stage_count, day, asleep: {navId: stage} },
|
|
396
|
+
// or null — and null is OFF, the hall as today.
|
|
397
|
+
founding_mode: foundingMode,
|
|
381
398
|
// task 1003744: { '<atom>': boolean } for each permission key a hall nav
|
|
382
399
|
// item may gate on (NAV_PERMISSION_ATOMS above), so an item can declare the
|
|
383
400
|
// SAME atom its page is gated on instead of borrowing the nearest rank
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.20.
|
|
78
|
+
const CORE_VERSION = '1.20.48'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// tests/founding_mode.mjs — PROJECT FOUNDING MODE is a mode the existing hall wears,
|
|
2
|
+
// never a second hall (task 1004422 / BV2.PS03, goal 1000121; the blend rule in
|
|
3
|
+
// docs/design/project-startup-direction.md).
|
|
4
|
+
//
|
|
5
|
+
// The owner's no-hijack condition is the headline: a hall that is NOT founding must be
|
|
6
|
+
// the hall it was. So the client half is RUN, not grepped. The real applyFounding is
|
|
7
|
+
// sliced out of shell.js and driven over a small fake DOM, fed by the REAL server
|
|
8
|
+
// shaper, so the path from the stored stage to the dimmed nav item is exercised
|
|
9
|
+
// whole. Then OFF is proven to leave, and to restore, the DOM exactly.
|
|
10
|
+
import { strict as assert } from 'node:assert';
|
|
11
|
+
import { test } from 'node:test';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8').replace(/\r\n/g, '\n');
|
|
20
|
+
const fm = require(path.join(ROOT, 'modules', 'onboarding', 'founding-mode.js'));
|
|
21
|
+
|
|
22
|
+
const SHELL = read('modules', 'hall-ui', 'public', 'shell.js');
|
|
23
|
+
const HALL_RENDER = read('modules', 'hall-ui', 'public', 'hall-render.js');
|
|
24
|
+
const ME = read('src', 'bongos', 'routes', 'me.js');
|
|
25
|
+
const PORT = read('modules', 'onboarding', 'port.js');
|
|
26
|
+
|
|
27
|
+
// ── the server shaper (pure) ────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
test('not founding in any form is null (OFF)', () => {
|
|
30
|
+
assert.equal(fm.shapeFoundingMode(null), null);
|
|
31
|
+
assert.equal(fm.shapeFoundingMode({ active: null }), null);
|
|
32
|
+
assert.equal(fm.shapeFoundingMode({ active: 'false', stage: 'scope' }), null);
|
|
33
|
+
assert.equal(fm.shapeFoundingMode({ active: 'maybe', stage: 'scope' }), null);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('an unknown stage fails CLOSED — OFF, never a hall stuck half-asleep', () => {
|
|
37
|
+
assert.equal(fm.shapeFoundingMode({ active: 'true', stage: 'launch' }), null);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('active with no stage yet is the decide pre-stage, and every room sleeps', () => {
|
|
41
|
+
const f = fm.shapeFoundingMode({ active: 'true' });
|
|
42
|
+
assert.equal(f.stage, 'decide');
|
|
43
|
+
assert.equal(f.stage_number, 0);
|
|
44
|
+
assert.equal(f.stage_count, 5);
|
|
45
|
+
for (const id of ['roadmap', 'goals', 'goal-map', 'work', 'board-room', 'economy', 'ranks', 'collab', 'roster']) {
|
|
46
|
+
assert.ok(id in f.asleep, `${id} sleeps while deciding`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('a room wakes once the stage that wakes it is behind', () => {
|
|
51
|
+
const f = fm.shapeFoundingMode({ active: 'true', stage: 'charter' });
|
|
52
|
+
for (const id of ['roadmap', 'goals', 'goal-map', 'work']) assert.ok(!(id in f.asleep), `${id} woke`);
|
|
53
|
+
assert.equal(f.asleep['board-room'], 'charter');
|
|
54
|
+
assert.equal(f.asleep.economy, 'legalize');
|
|
55
|
+
assert.equal(f.asleep.ranks, 'legislation');
|
|
56
|
+
assert.equal(f.asleep.collab, 'alive', 'community wakes only when the planet is alive');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('the day counts from the founding day; an unreadable timestamp drops it', () => {
|
|
60
|
+
const now = new Date('2026-10-09T12:00:00Z');
|
|
61
|
+
assert.equal(fm.dayOf('2026-10-01T09:00:00Z', now), 9);
|
|
62
|
+
assert.equal(fm.dayOf('not a date', now), null);
|
|
63
|
+
assert.equal(fm.dayOf(null, now), null);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('foundingModeFor fails closed when the settings read throws', async () => {
|
|
67
|
+
const got = await fm.foundingModeFor({ getSetting: async () => { throw new Error('db down'); } });
|
|
68
|
+
assert.equal(got, null);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('setFoundingMode refuses an unknown stage and stamps started_at once', async () => {
|
|
72
|
+
const store = new Map();
|
|
73
|
+
const deps = {
|
|
74
|
+
getSetting: async (k) => (store.has(k) ? { key: k, value: store.get(k) } : null),
|
|
75
|
+
setSetting: async (k, v) => { store.set(k, String(v)); return { key: k, value: String(v) }; },
|
|
76
|
+
now: new Date('2026-10-01T00:00:00Z'),
|
|
77
|
+
};
|
|
78
|
+
await assert.rejects(fm.setFoundingMode({ stage: 'launch' }, 1, deps), /unknown founding stage/);
|
|
79
|
+
const first = await fm.setFoundingMode({ active: true, stage: 'decide' }, 1, deps);
|
|
80
|
+
assert.equal(first.stage, 'decide');
|
|
81
|
+
const stamp = store.get(fm.KEYS.STARTED_AT);
|
|
82
|
+
deps.now = new Date('2026-10-05T00:00:00Z');
|
|
83
|
+
await fm.setFoundingMode({ active: true, stage: 'scope' }, 1, deps);
|
|
84
|
+
assert.equal(store.get(fm.KEYS.STARTED_AT), stamp, 'started_at is not re-stamped');
|
|
85
|
+
assert.equal(await fm.setFoundingMode({ active: false }, 1, deps), null, 'switching off reads back as OFF');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('every room the server can put to sleep is a real NAV item', () => {
|
|
89
|
+
// The map is keyed by NAV id so the client never re-derives it; a renamed NAV id
|
|
90
|
+
// would silently leave a room awake. Hold the two together.
|
|
91
|
+
const nav = SHELL.slice(SHELL.indexOf('var NAV = ['), SHELL.indexOf('var NAV_COLLAPSE_KEY'));
|
|
92
|
+
const ids = new Set([...nav.matchAll(/id: '([a-z-]+)'/g)].map((m) => m[1]));
|
|
93
|
+
const all = [...Object.values(fm.WAKES).flat(), ...fm.WAKES_WHEN_ALIVE];
|
|
94
|
+
for (const id of all) assert.ok(ids.has(id), `NAV has no item '${id}'`);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ── the client, RUN over a fake DOM ─────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
class El {
|
|
100
|
+
constructor(tag) {
|
|
101
|
+
this.tag = tag; this.id = ''; this.className = ''; this.dataset = {}; this.attrs = {};
|
|
102
|
+
this.children = []; this.parent = null; this.textContent = ''; this.innerHTML = '';
|
|
103
|
+
const self = this;
|
|
104
|
+
this.classList = {
|
|
105
|
+
add(c) { const s = new Set(self.className.split(' ').filter(Boolean)); s.add(c); self.className = [...s].join(' '); },
|
|
106
|
+
remove(c) { self.className = self.className.split(' ').filter((x) => x && x !== c).join(' '); },
|
|
107
|
+
contains(c) { return self.className.split(' ').includes(c); },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
appendChild(c) { c.parent = this; this.children.push(c); return c; }
|
|
111
|
+
insertBefore(c, ref) { c.parent = this; const i = this.children.indexOf(ref); this.children.splice(i < 0 ? 0 : i, 0, c); return c; }
|
|
112
|
+
get parentNode() { return this.parent; }
|
|
113
|
+
removeChild(c) { this.children = this.children.filter((x) => x !== c); c.parent = null; return c; }
|
|
114
|
+
setAttribute(k, v) { this.attrs[k] = v; }
|
|
115
|
+
querySelector(sel) { return sel === '[data-wakes-tag]' ? (this.children.find((c) => 'data-wakes-tag' in c.attrs) || null) : null; }
|
|
116
|
+
get firstChild() { return this.children[0] || null; }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function makeHall() {
|
|
120
|
+
const navIds = ['home', 'roadmap', 'goals', 'goal-map', 'work', 'board-room', 'economy', 'ranks', 'collab', 'roster', 'profile'];
|
|
121
|
+
const nav = navIds.map((id) => { const a = new El('a'); a.id = 'nav-' + id; a.className = 'nav-item'; return a; });
|
|
122
|
+
const appBody = new El('div');
|
|
123
|
+
const topbar = new El('header'); topbar.id = 'app-topbar';
|
|
124
|
+
appBody.appendChild(topbar);
|
|
125
|
+
const root = { dataset: {} };
|
|
126
|
+
const document = {
|
|
127
|
+
documentElement: root,
|
|
128
|
+
createElement: (t) => new El(t),
|
|
129
|
+
querySelector: (sel) => (sel === '.app-body' ? appBody : null),
|
|
130
|
+
querySelectorAll: (sel) => (sel === '#app-sidebar .nav-item--asleep' ? nav.filter((a) => a.classList.contains('nav-item--asleep')) : []),
|
|
131
|
+
getElementById: (id) => nav.find((a) => a.id === id) || appBody.children.find((c) => c.id === id) || null,
|
|
132
|
+
};
|
|
133
|
+
return { document, nav, appBody, root };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The REAL escaper and the real wake map are sliced out of shell.js with the function,
|
|
137
|
+
// so the band's escaping is the shell's own, not a stand-in that agrees with the test.
|
|
138
|
+
function sliceFn(name) {
|
|
139
|
+
const start = SHELL.indexOf(' function ' + name + '(');
|
|
140
|
+
const end = SHELL.indexOf('\n }\n', start);
|
|
141
|
+
assert.ok(start > -1 && end > start, name + ' was located in shell.js');
|
|
142
|
+
return SHELL.slice(start, end + 4);
|
|
143
|
+
}
|
|
144
|
+
function loadApplyFounding(document, access, brand = { identity: { productName: 'Kestrel' } }) {
|
|
145
|
+
const wakeStart = SHELL.indexOf(' var WAKE_TEXT = {');
|
|
146
|
+
const wakeEnd = SHELL.indexOf('};', wakeStart) + 2;
|
|
147
|
+
assert.ok(wakeStart > -1, 'WAKE_TEXT was located in shell.js');
|
|
148
|
+
const src = [SHELL.slice(wakeStart, wakeEnd), sliceFn('esc'), sliceFn('applyFounding')].join('\n');
|
|
149
|
+
return new Function('document', 'brand', 'access', `${src}\nreturn applyFounding;`)(document, brand, access);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const snapshot = (h) => JSON.stringify({
|
|
153
|
+
root: h.root.dataset,
|
|
154
|
+
body: h.appBody.children.map((c) => c.id),
|
|
155
|
+
nav: h.nav.map((a) => ({ id: a.id, cls: a.className, data: a.dataset, kids: a.children.length })),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('THE NO-HIJACK CONDITION: a hall that is not founding is untouched', () => {
|
|
159
|
+
const h = makeHall();
|
|
160
|
+
const before = snapshot(h);
|
|
161
|
+
const access = { founding: null };
|
|
162
|
+
loadApplyFounding(h.document, access)();
|
|
163
|
+
assert.equal(snapshot(h), before, 'null founding changes nothing at all');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('not founding touches NO node — the document is never even queried', () => {
|
|
167
|
+
// Stronger than "ends up the same": a page that never wore the mode is not read.
|
|
168
|
+
const h = makeHall();
|
|
169
|
+
const touched = [];
|
|
170
|
+
const spy = new Proxy(h.document, {
|
|
171
|
+
get(t, k) { if (k !== 'documentElement') touched.push(String(k)); return t[k]; },
|
|
172
|
+
});
|
|
173
|
+
loadApplyFounding(spy, { founding: null })();
|
|
174
|
+
loadApplyFounding(spy, { founding: { active: false } })();
|
|
175
|
+
assert.deepEqual(touched, [], 'no DOM method was called');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('founding ON: the band above the topbar, and the rooms the server named asleep', () => {
|
|
179
|
+
const h = makeHall();
|
|
180
|
+
const now = new Date('2026-10-09T12:00:00Z');
|
|
181
|
+
const access = { founding: fm.shapeFoundingMode({ active: 'true', stage: 'strategize', startedAt: '2026-10-01T09:00:00Z' }, now) };
|
|
182
|
+
loadApplyFounding(h.document, access)();
|
|
183
|
+
assert.equal(h.root.dataset.founding, 'on');
|
|
184
|
+
const band = h.appBody.children[0];
|
|
185
|
+
assert.equal(band.id, 'founding-band', 'the band is the first child of .app-body, above the topbar');
|
|
186
|
+
assert.equal(h.appBody.children[1].id, 'app-topbar', 'the topbar itself is kept, not replaced');
|
|
187
|
+
assert.match(band.innerHTML, /Project founding mode/);
|
|
188
|
+
assert.match(band.innerHTML, /Kestrel is still being founded/);
|
|
189
|
+
assert.match(band.innerHTML, /stage 2 of 5 · strategize · day 9/);
|
|
190
|
+
const byId = (id) => h.nav.find((a) => a.id === 'nav-' + id);
|
|
191
|
+
assert.ok(!byId('goals').classList.contains('nav-item--asleep'), 'scope is behind: goals is awake');
|
|
192
|
+
assert.ok(byId('work').classList.contains('nav-item--asleep'), 'strategize is current: tasks still sleeps');
|
|
193
|
+
assert.equal(byId('work').children[0].textContent, 'opens after strategy', 'a sentence, never a raw slug');
|
|
194
|
+
assert.equal(byId('collab').children[0].textContent, 'opens when alive');
|
|
195
|
+
assert.ok(!byId('home').classList.contains('nav-item--asleep'), 'home is never put to sleep');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('founding OFF after ON restores the hall exactly — OFF removes, it does not hide', () => {
|
|
199
|
+
const h = makeHall();
|
|
200
|
+
const before = snapshot(h);
|
|
201
|
+
const access = { founding: fm.shapeFoundingMode({ active: 'true', stage: 'decide' }) };
|
|
202
|
+
const apply = loadApplyFounding(h.document, access);
|
|
203
|
+
apply();
|
|
204
|
+
assert.notEqual(snapshot(h), before);
|
|
205
|
+
access.founding = null;
|
|
206
|
+
apply();
|
|
207
|
+
assert.equal(snapshot(h), before, 'band, asleep classes, wake tags and the root attribute are all gone');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('the band escapes what it prints — a project name cannot inject markup', () => {
|
|
211
|
+
const h = makeHall();
|
|
212
|
+
const access = { founding: Object.assign(fm.shapeFoundingMode({ active: 'true', stage: 'scope' }), { waiting_on: ['<img src=x onerror=1>'] }) };
|
|
213
|
+
loadApplyFounding(h.document, access, { identity: { productName: '<script>alert(1)</script>' } })();
|
|
214
|
+
const html = h.appBody.children[0].innerHTML;
|
|
215
|
+
assert.ok(!html.includes('<script>'), 'the name is escaped');
|
|
216
|
+
assert.ok(!html.includes('<img'), 'the waiting-on names are escaped');
|
|
217
|
+
assert.match(html, /<script>/);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('who the stage close is waiting on shows in the band when the server sends it', async () => {
|
|
221
|
+
const f = await fm.foundingModeFor({
|
|
222
|
+
getSetting: async (k) => ({ [fm.KEYS.ACTIVE]: { value: 'true' }, [fm.KEYS.STAGE]: { value: 'charter' } }[k] || null),
|
|
223
|
+
waitingOnFor: async (stage) => (stage === 'charter' ? ['Avery', 'Jordan'] : []),
|
|
224
|
+
});
|
|
225
|
+
assert.deepEqual(f.waiting_on, ['Avery', 'Jordan']);
|
|
226
|
+
const h = makeHall();
|
|
227
|
+
loadApplyFounding(h.document, { founding: f })();
|
|
228
|
+
assert.match(h.appBody.children[0].innerHTML, /waiting on Avery, Jordan/);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('waiting-on reads the government port\'s genesis-stage status, sitting stages only', async () => {
|
|
232
|
+
const gov = {
|
|
233
|
+
genesisStageStatus: async () => ({ stages: [
|
|
234
|
+
{ key: 'scope', state: 'passed', waiting_on: [] },
|
|
235
|
+
{ key: 'charter', state: 'sitting', waiting_on: [{ display_name: 'Avery' }, { github_login: 'jordan' }] },
|
|
236
|
+
{ key: 'legalize', state: 'not_put', waiting_on: [] },
|
|
237
|
+
] }),
|
|
238
|
+
};
|
|
239
|
+
const read = (stage) => fm.foundingModeFor({
|
|
240
|
+
getSetting: async (k) => ({ [fm.KEYS.ACTIVE]: { value: 'true' }, [fm.KEYS.STAGE]: { value: stage } }[k] || null),
|
|
241
|
+
government: gov,
|
|
242
|
+
});
|
|
243
|
+
assert.deepEqual((await read('charter')).waiting_on, ['Avery', 'jordan']);
|
|
244
|
+
assert.equal((await read('scope')).waiting_on, null, 'a passed stage waits on nobody');
|
|
245
|
+
assert.equal(fm.waitingOnFromGovernment({}), null, 'a port without the read offers none');
|
|
246
|
+
assert.equal((await fm.foundingModeFor({
|
|
247
|
+
getSetting: async (k) => ({ [fm.KEYS.ACTIVE]: { value: 'true' } }[k] || null), government: null,
|
|
248
|
+
})).waiting_on, null, 'no government module: the mode still reads, without names');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('a failing waiting-on source never turns the mode off', async () => {
|
|
252
|
+
const f = await fm.foundingModeFor({
|
|
253
|
+
getSetting: async (k) => ({ [fm.KEYS.ACTIVE]: { value: 'true' }, [fm.KEYS.STAGE]: { value: 'scope' } }[k] || null),
|
|
254
|
+
waitingOnFor: async () => { throw new Error('government down'); },
|
|
255
|
+
});
|
|
256
|
+
assert.equal(f.active, true);
|
|
257
|
+
assert.equal(f.waiting_on, null);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('re-applying ON does not stack a second band or duplicate wake tags', () => {
|
|
261
|
+
const h = makeHall();
|
|
262
|
+
const access = { founding: fm.shapeFoundingMode({ active: 'true', stage: 'decide' }) };
|
|
263
|
+
const apply = loadApplyFounding(h.document, access);
|
|
264
|
+
apply(); apply();
|
|
265
|
+
assert.equal(h.appBody.children.filter((c) => c.id === 'founding-band').length, 1);
|
|
266
|
+
assert.equal(h.nav.find((a) => a.id === 'nav-work').children.length, 1);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ── the wiring ──────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
test('only an explicit active:true turns the mode on', () => {
|
|
272
|
+
const fn = SHELL.slice(SHELL.indexOf(' function applyFounding()'));
|
|
273
|
+
assert.match(fn.slice(0, 400), /f && f\.active === true/);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('/me carries founding_mode from the onboarding port, fail-open to null', () => {
|
|
277
|
+
assert.match(ME, /founding_mode: foundingMode/);
|
|
278
|
+
assert.match(ME, /onboarding\.foundingModeFor\(\)/);
|
|
279
|
+
const at = ME.indexOf('onboarding.foundingModeFor()');
|
|
280
|
+
assert.match(ME.slice(at, at + 200), /catch \(_\) \{ return null; \}/);
|
|
281
|
+
assert.match(PORT, /foundingModeFor: \(\.\.\.a\) => foundingMode\.foundingModeFor\(\.\.\.a\)/);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('BOTH applyAccess callers pass it, or the index would never show the band', () => {
|
|
285
|
+
assert.match(SHELL, /founding: me\.founding_mode \|\| null/);
|
|
286
|
+
assert.match(HALL_RENDER, /founding: \(ctx\.lastMeState && ctx\.lastMeState\.founding_mode\) \|\| null/);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('founding mode adds no page, no route and no nav item — it is a mode', () => {
|
|
290
|
+
const nav = SHELL.slice(SHELL.indexOf('var NAV = ['), SHELL.indexOf('var NAV_COLLAPSE_KEY'));
|
|
291
|
+
assert.ok(!/founding|genesis/i.test(nav), 'the NAV model must not grow a founding entry');
|
|
292
|
+
const hall = fs.readdirSync(path.join(ROOT, 'modules', 'hall-ui', 'public'));
|
|
293
|
+
assert.ok(!hall.some((f) => /^founding\./.test(f)), 'no founding page file');
|
|
294
|
+
});
|
|
@@ -635,7 +635,7 @@ try {
|
|
|
635
635
|
assert.match(HTML, /<body data-page="tweak-editor"[^>]*data-fullscreen-switch/);
|
|
636
636
|
assert.equal((HTML.match(/<h1\b/g) || []).length, 1);
|
|
637
637
|
assert.ok(HTML.indexOf('tweak-editor-lib.js') < HTML.indexOf('tweak-editor.js?'), 'the lib loads first');
|
|
638
|
-
assert.match(HTML, /shell\.js\?v=2026-09-30-commands/);
|
|
638
|
+
assert.match(HTML, /shell\.js\?v=2026-09-30-founding-commands/);
|
|
639
639
|
assert.equal(/fonts\.googleapis\.com\/css/.test(HTML), false);
|
|
640
640
|
for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-open-more', 'tw-divider', 'tw-openwrap', 'tw-open-menu', 'tw-open-tab', 'tw-open-split', 'tw-frame', 'tw-frame-view', 'tw-split-close', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
|
|
641
641
|
assert.ok(new RegExp(`id="${id}"`).test(HTML), `#${id} is in the markup`);
|
|
@@ -87,7 +87,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
87
87
|
// the file and left the query alone, so a returning browser kept the cached copy —
|
|
88
88
|
// a fix that reaches nobody looks identical to a fix that does not work.
|
|
89
89
|
|
|
90
|
-
const SHELL_SHA = '
|
|
90
|
+
const SHELL_SHA = '3e4f905947bc39d2';
|
|
91
91
|
|
|
92
92
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
93
93
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|