@bongos/core 1.21.17 → 1.21.18
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 +17 -17
- package/docs/adr/0364-repo-access-is-asked-at-sign-in-and-held-for-the-session.md +5 -1
- package/docs/copy-inventory.md +379 -381
- package/docs/copy-registry.json +455 -473
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +233 -233
- package/modules/public-landing/public/projects.html +34 -37
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/wizard_draft_resume.mjs +59 -30
- package/tests/wizard_six_screens.mjs +1 -1
|
@@ -590,7 +590,6 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
590
590
|
|
|
591
591
|
/* GitHub repo picker (task 1002099): hairline rows, the chosen one marked on
|
|
592
592
|
the accent; the private badge in the label face */
|
|
593
|
-
#ghConnect .btn{margin-top:4px;}
|
|
594
593
|
.ghlist{margin-top:4px;max-height:220px;overflow-y:auto;border:0;border-top:1px solid var(--rule);border-radius:0;background:none;}
|
|
595
594
|
.ghlist:empty{display:none;}
|
|
596
595
|
.ghrow{display:flex;align-items:center;gap:8px;width:100%;min-height:40px;text-align:left;background:none;border:0;border-bottom:1px solid var(--rule-soft);padding:10px 0;font:inherit;font-size:14px;color:var(--ink);cursor:pointer;}
|
|
@@ -1637,11 +1636,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1637
1636
|
<!-- GitHub-integrated repo picker (adopt) + auto-create (fresh) — task 1002099.
|
|
1638
1637
|
Without repo access we fall back to the manual owner/repo inputs
|
|
1639
1638
|
(#manualHome), which stay authoritative. -->
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1639
|
+
<!-- no repo-access ask in the wizard (task 1004555): every hub sign-in asks for repo
|
|
1640
|
+
access (ADR 0364), and a missing token is fetched by one automatic GitHub
|
|
1641
|
+
round-trip. Only when that hop could not help does this line show, above
|
|
1642
|
+
the manual fields. -->
|
|
1643
|
+
<div id="ghNoAccess" class="hint" hidden>We couldn’t read your GitHub repositories just now, so enter the repo by hand below.</div>
|
|
1645
1644
|
<div id="ghBusy" class="hint" hidden>Reading your GitHub repositories…</div>
|
|
1646
1645
|
<div id="ghAdopt" class="wizField" hidden>
|
|
1647
1646
|
<span>Pick one of your repositories</span>
|
|
@@ -1667,8 +1666,6 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1667
1666
|
</label>
|
|
1668
1667
|
<!-- visibility stated BEFORE GitHub's own radio is the first place it appears (task 1002721) -->
|
|
1669
1668
|
<div class="hint" style="grid-column:1 / -1;">Creating the repo yourself on GitHub? Public or private is your choice there — both work here, and we never change it.</div>
|
|
1670
|
-
<!-- the way back from the manual fallback (task 1003683) -->
|
|
1671
|
-
<button type="button" class="linklike" id="ghManualBack" style="grid-column:1 / -1;" hidden>Use my GitHub repositories instead</button>
|
|
1672
1669
|
</div>
|
|
1673
1670
|
<div class="hint" id="h-home"> </div>
|
|
1674
1671
|
<button type="button" class="btn small rowUse" data-row-use>Use this</button>
|
|
@@ -5276,44 +5273,39 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5276
5273
|
403 → offer to grant repo access (a ?repos=1 re-sign-in);
|
|
5277
5274
|
503 (server can't store the token) or any error → the manual inputs.
|
|
5278
5275
|
#manualHome (the classic owner/repo fields) is the always-safe fallback. ── */
|
|
5279
|
-
var repoAccess = null; /* null=unknown/loading · true · '
|
|
5276
|
+
var repoAccess = null; /* null=unknown/loading · true · 'unavailable' (manual fields) */
|
|
5280
5277
|
var repoList = []; /* the user's repos (from the probe) — feeds the picker + the fresh-name clash hint */
|
|
5281
5278
|
var ghProbed = false;
|
|
5282
5279
|
|
|
5283
|
-
function isManualHome() { return repoAccess === '
|
|
5280
|
+
function isManualHome() { return repoAccess === 'unavailable'; }
|
|
5284
5281
|
function validRepoName(s) { return /^[A-Za-z0-9._-]{1,100}$/.test(String(s || '')); }
|
|
5285
5282
|
function myLogin() { return (me && me.github_login) || 'you'; }
|
|
5286
5283
|
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5284
|
+
/* The one automatic GitHub round-trip (task 1004555). The token is missing (a session
|
|
5285
|
+
from before ADR 0364, or a sign-out elsewhere), and GitHub answers a repo sign-in at
|
|
5286
|
+
once for anyone who already approved it — so the page goes and fetches it instead of
|
|
5287
|
+
asking. Same tab, so the sessionStorage draft comes back with its step (task 1004549).
|
|
5288
|
+
ONCE per tab session: a hop that came back still without access cannot be helped by a
|
|
5289
|
+
second, so the caller falls back to the manual fields instead of looping. A probe that
|
|
5290
|
+
answers 200 clears the mark, so a later expiry may hop again. */
|
|
5291
|
+
var HOP_KEY = 'cb-repo-hop';
|
|
5292
|
+
function repoHopDone() { try { return sessionStorage.getItem(HOP_KEY) === '1'; } catch (e) { return true; } }
|
|
5293
|
+
function hopForRepoAccess() {
|
|
5294
|
+
if (repoHopDone()) return false;
|
|
5295
|
+
try { sessionStorage.setItem(HOP_KEY, '1'); } catch (e) { return false; }
|
|
5296
|
+
saveDraft();
|
|
5290
5297
|
window.location.assign(API + '/auth/web/start?return=/projects%3Fview%3Dnew&repos=1');
|
|
5298
|
+
return true;
|
|
5291
5299
|
}
|
|
5292
|
-
$('ghConnectBtn').addEventListener('click', connectRepos);
|
|
5293
|
-
$('ghManualToggle').addEventListener('click', function () { repoAccess = 'manual'; syncHome(); $('f-owner').focus(); });
|
|
5294
|
-
$('ghManualBack').addEventListener('click', function () { repoAccess = 'need-connect'; syncHome(); $('ghConnectBtn').focus(); });
|
|
5295
5300
|
|
|
5296
5301
|
function syncHome() {
|
|
5297
|
-
var need = repoAccess === 'need-connect';
|
|
5298
5302
|
var ok = repoAccess === true;
|
|
5299
5303
|
var adopt = state.mode === 'adopt';
|
|
5300
5304
|
$('ghBusy').hidden = !!repoAccess; /* busy only while still probing (null) */
|
|
5301
|
-
$('
|
|
5305
|
+
$('ghNoAccess').hidden = repoAccess !== 'unavailable';
|
|
5302
5306
|
$('ghAdopt').hidden = !(ok && adopt);
|
|
5303
5307
|
$('ghFresh').hidden = !(ok && !adopt);
|
|
5304
5308
|
$('manualHome').hidden = !isManualHome();
|
|
5305
|
-
/* only when the visitor CHOSE manual: with 'unavailable' the server cannot hold a
|
|
5306
|
-
token, so a connect round-trip would land back here having changed nothing */
|
|
5307
|
-
$('ghManualBack').hidden = repoAccess !== 'manual';
|
|
5308
|
-
if (need) {
|
|
5309
|
-
/* Informed consent (ADR 0155): this grants `repo`, which GitHub's own screen
|
|
5310
|
-
words as full control of ALL your repositories — so say plainly why we ask
|
|
5311
|
-
and what we do with it, rather than letting GitHub's wording be the first
|
|
5312
|
-
the user hears of it. Private repos are the reason the scope is this wide. */
|
|
5313
|
-
$('ghConnectMsg').textContent = adopt
|
|
5314
|
-
? 'You’re signed in — creating your project needs one more GitHub permission. GitHub will ask for repository access (it words this as access to all your repositories — that is the only scope it offers that can write to a private repo). We use it to read the repo you pick and push the setup commit to it. Nothing else is touched.'
|
|
5315
|
-
: 'You’re signed in — creating your project needs one more GitHub permission. GitHub will ask for repository access (it words this as access to all your repositories — the narrower option cannot write to private repos). We use it to create your new empty repo and push the setup commit to it. Nothing else is touched.';
|
|
5316
|
-
}
|
|
5317
5309
|
if (ok && adopt) initAdoptPicker();
|
|
5318
5310
|
if (ok && !adopt) syncFresh();
|
|
5319
5311
|
}
|
|
@@ -5413,13 +5405,16 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5413
5405
|
|
|
5414
5406
|
function ensureRepoAccess() {
|
|
5415
5407
|
if (ghProbed && repoAccess !== null) return; /* already known this session */
|
|
5416
|
-
if (isManualHome()) return; /* user chose manual — don't nag */
|
|
5417
5408
|
repoAccess = null; syncHome(); /* show "reading your repos…" */
|
|
5418
5409
|
fetch(API + '/github/repos', { credentials: 'same-origin' })
|
|
5419
5410
|
.then(function (res) {
|
|
5420
5411
|
ghProbed = true;
|
|
5421
|
-
if (res.status === 200)
|
|
5422
|
-
|
|
5412
|
+
if (res.status === 200) {
|
|
5413
|
+
try { sessionStorage.removeItem(HOP_KEY); } catch (e) {}
|
|
5414
|
+
return res.json().then(function (d) { repoList = (d && d.repos) || []; repoAccess = true; });
|
|
5415
|
+
}
|
|
5416
|
+
/* no stored token: fetch it (one hop), else the manual fields */
|
|
5417
|
+
if (res.status === 403 || res.status === 401) { if (!hopForRepoAccess()) repoAccess = 'unavailable'; return; }
|
|
5423
5418
|
repoAccess = 'unavailable'; return; /* 503 or anything else → manual fallback */
|
|
5424
5419
|
})
|
|
5425
5420
|
.catch(function () { ghProbed = true; repoAccess = 'unavailable'; })
|
|
@@ -5901,7 +5896,6 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5901
5896
|
if (!works.length) return homeErr('Pick what already exists, then your main one.');
|
|
5902
5897
|
if (works.indexOf(a.main_work) === -1) return homeErr('Mark which of them is your main one.');
|
|
5903
5898
|
}
|
|
5904
|
-
if (repoAccess === 'need-connect') return homeErr('Allow GitHub repo access to pick or create your repository — or choose “enter it manually”.');
|
|
5905
5899
|
if (repoAccess === true && state.mode !== 'adopt') {
|
|
5906
5900
|
/* Fresh with access: just the new repo NAME (owner is the signed-in account). */
|
|
5907
5901
|
if (!validRepoName(state.repo)) return homeErr('Name the new repository (letters, numbers, and . _ -).');
|
|
@@ -6346,9 +6340,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6346
6340
|
}).then(function (res) {
|
|
6347
6341
|
return res.json().catch(function () { return {}; }).then(function (data) {
|
|
6348
6342
|
if (res.status === 401 || res.status === 403) {
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6343
|
+
/* the token went between the probe and Create: the hop fetches it again and
|
|
6344
|
+
brings the founder back to this screen; if it cannot, the manual fields */
|
|
6345
|
+
var hopped = hopForRepoAccess();
|
|
6346
|
+
if (!hopped) { repoAccess = 'unavailable'; syncHome(); }
|
|
6347
|
+
var e0 = new Error('reconnect'); e0.gotoHome = !hopped;
|
|
6348
|
+
e0.homeMsg = 'We couldn’t create the repo on GitHub just now — enter one you made yourself below.';
|
|
6352
6349
|
throw e0;
|
|
6353
6350
|
}
|
|
6354
6351
|
if (!res.ok) {
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.18",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.18",
|
|
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.21.
|
|
3
|
+
"version": "1.21.18",
|
|
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
|
@@ -8858,5 +8858,11 @@
|
|
|
8858
8858
|
"id": "1004554",
|
|
8859
8859
|
"text": "Bongos no longer plays any sounds: no ship bell, credits chime, fanfare or end-of-turn chime. Settings → Sound now just says they're off for now."
|
|
8860
8860
|
}
|
|
8861
|
+
],
|
|
8862
|
+
"1.21.18": [
|
|
8863
|
+
{
|
|
8864
|
+
"id": "1004555",
|
|
8865
|
+
"text": "Project setup no longer shows an \"Allow repo access\" button — if the site needs GitHub access it fetches it in a quick reload and brings you straight back to where you were."
|
|
8866
|
+
}
|
|
8861
8867
|
]
|
|
8862
8868
|
}
|
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.21.
|
|
78
|
+
const CORE_VERSION = '1.21.18'; // 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');
|
|
@@ -101,57 +101,86 @@ test('a draft already at step 1 is unchanged, and an out-of-range step is not re
|
|
|
101
101
|
}
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
-
// ── slice 2 ·
|
|
104
|
+
// ── slice 2 · no "Allow repo access" step: the token is fetched, not asked for (task 1004555) ──
|
|
105
|
+
// Owner, 2026-10-02: "I don't want to see the old button at all." Every hub sign-in asks
|
|
106
|
+
// for repo access (ADR 0364) and GitHub answers at once for anyone who approved it, so a
|
|
107
|
+
// missing token is fetched by ONE automatic round-trip. The slices below are syncHome (what
|
|
108
|
+
// shows) and ensureRepoAccess (the probe and the hop), run under a DOM + fetch stub.
|
|
105
109
|
const SYNC_RE = /function syncHome\(\) \{[\s\S]*?\n {4}\}/;
|
|
106
110
|
const syncSlice = (HUB.match(SYNC_RE) || [null])[0];
|
|
111
|
+
const HOP_RE = /var HOP_KEY = 'cb-repo-hop';[\s\S]*?function hopForRepoAccess\(\) \{[\s\S]*?\n {4}\}/;
|
|
112
|
+
const hopSlice = (HUB.match(HOP_RE) || [null])[0];
|
|
113
|
+
const PROBE_RE = /function ensureRepoAccess\(\) \{[\s\S]*?\n {4}\}/;
|
|
114
|
+
const probeSlice = (HUB.match(PROBE_RE) || [null])[0];
|
|
107
115
|
|
|
108
|
-
test('the syncHome
|
|
116
|
+
test('the syncHome, hop and probe slices are still findable', () => {
|
|
109
117
|
assert.ok(syncSlice, 'syncHome() was not found in projects.html');
|
|
110
|
-
assert.
|
|
118
|
+
assert.ok(hopSlice, 'hopForRepoAccess() was not found in projects.html');
|
|
119
|
+
assert.ok(probeSlice, 'ensureRepoAccess() was not found in projects.html');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('the page carries no "Allow repo access" control at all', () => {
|
|
123
|
+
assert.doesNotMatch(HUB, /Allow repo access/);
|
|
124
|
+
assert.doesNotMatch(HUB, /id="ghConnect/);
|
|
125
|
+
assert.doesNotMatch(HUB, /need-connect/, 'no state is left that would ask for it');
|
|
111
126
|
});
|
|
112
127
|
|
|
113
128
|
function runSync(repoAccess) {
|
|
114
129
|
const els = {};
|
|
115
130
|
// Elements start HIDDEN so that "shown" is only ever the result of syncHome setting it.
|
|
116
|
-
|
|
117
|
-
// the assertion below would pass against the unfixed page.
|
|
118
|
-
const el = (id) => (els[id] ||= {
|
|
119
|
-
id, hidden: true, textContent: '', _on: false,
|
|
120
|
-
classList: { toggle(_c, v) { els[id]._on = !!v; } },
|
|
121
|
-
});
|
|
131
|
+
const el = (id) => (els[id] ||= { id, hidden: true, textContent: '' });
|
|
122
132
|
const sandbox = {
|
|
123
|
-
repoAccess,
|
|
124
|
-
|
|
125
|
-
$: el,
|
|
126
|
-
isManualHome: () => repoAccess === 'manual' || repoAccess === 'unavailable',
|
|
133
|
+
repoAccess, state: { mode: 'greenfield' }, $: el,
|
|
134
|
+
isManualHome: () => repoAccess === 'unavailable',
|
|
127
135
|
initAdoptPicker() {}, syncFresh() {},
|
|
128
136
|
};
|
|
129
137
|
sandbox.globalThis = sandbox;
|
|
130
138
|
vm.createContext(sandbox);
|
|
131
139
|
vm.runInContext(syncSlice + '\nsyncHome();', sandbox);
|
|
132
|
-
return {
|
|
140
|
+
return { noteShown: !el('ghNoAccess').hidden, manualShown: !el('manualHome').hidden, freshShown: !el('ghFresh').hidden };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
test('without access the manual fields show with one plain line; with access they do not', () => {
|
|
144
|
+
assert.deepEqual(runSync('unavailable'), { noteShown: true, manualShown: true, freshShown: false });
|
|
145
|
+
assert.deepEqual(runSync(true), { noteShown: false, manualShown: false, freshShown: true });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
async function runProbe({ status, hopped = false }) {
|
|
149
|
+
const store = new Map(hopped ? [['cb-repo-hop', '1']] : []);
|
|
150
|
+
const assigned = [];
|
|
151
|
+
let saves = 0;
|
|
152
|
+
const sandbox = {
|
|
153
|
+
API: '/api/bongos', ghProbed: false, repoAccess: null, repoList: [],
|
|
154
|
+
sessionStorage: { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, v), removeItem: (k) => store.delete(k) },
|
|
155
|
+
window: { location: { assign: (u) => assigned.push(u) } },
|
|
156
|
+
saveDraft() { saves += 1; }, syncHome() {},
|
|
157
|
+
fetch: () => Promise.resolve({ status, json: () => Promise.resolve({ repos: [] }) }),
|
|
158
|
+
};
|
|
159
|
+
sandbox.globalThis = sandbox;
|
|
160
|
+
vm.createContext(sandbox);
|
|
161
|
+
vm.runInContext(hopSlice + '\n' + probeSlice + '\nvar done = ensureRepoAccess();', sandbox);
|
|
162
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
163
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
164
|
+
return { assigned, saves, repoAccess: vm.runInContext('repoAccess', sandbox), mark: store.get('cb-repo-hop') || null };
|
|
133
165
|
}
|
|
134
166
|
|
|
135
|
-
test('
|
|
136
|
-
const
|
|
137
|
-
assert.
|
|
138
|
-
assert.equal(
|
|
139
|
-
assert.equal(
|
|
167
|
+
test('a missing token is fetched by one automatic GitHub round-trip, back to the wizard, draft saved first', async () => {
|
|
168
|
+
const r = await runProbe({ status: 403 });
|
|
169
|
+
assert.deepEqual(r.assigned, ['/api/bongos/auth/web/start?return=/projects%3Fview%3Dnew&repos=1']);
|
|
170
|
+
assert.equal(r.saves, 1, 'the draft is saved before the page leaves');
|
|
171
|
+
assert.equal(r.mark, '1', 'the hop is marked for this tab');
|
|
140
172
|
});
|
|
141
173
|
|
|
142
|
-
test('a
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const forced = runSync('unavailable');
|
|
147
|
-
assert.equal(forced.manualShown, true);
|
|
148
|
-
assert.equal(forced.backShown, false);
|
|
174
|
+
test('a second miss after the hop falls back to the manual fields instead of looping', async () => {
|
|
175
|
+
const r = await runProbe({ status: 403, hopped: true });
|
|
176
|
+
assert.deepEqual(r.assigned, [], 'no second round-trip');
|
|
177
|
+
assert.equal(r.repoAccess, 'unavailable');
|
|
149
178
|
});
|
|
150
179
|
|
|
151
|
-
test('
|
|
152
|
-
const
|
|
153
|
-
assert.equal(
|
|
154
|
-
assert.equal(
|
|
180
|
+
test('access found clears the mark, so a later expiry may hop again', async () => {
|
|
181
|
+
const r = await runProbe({ status: 200, hopped: true });
|
|
182
|
+
assert.equal(r.repoAccess, true);
|
|
183
|
+
assert.equal(r.mark, null);
|
|
155
184
|
});
|
|
156
185
|
|
|
157
186
|
// ── slice 3 · why this task's original team-shape claim was wrong ─────────────────
|
|
@@ -102,7 +102,7 @@ test('nothing the old steps could do is lost: their controls are moved whole int
|
|
|
102
102
|
const r = (p.match(new RegExp(`<div class="setRow" data-row="${row}">[\\s\\S]*?\\n {16}</div>\\n`)) || [''])[0];
|
|
103
103
|
for (const id of ids) assert.match(r, new RegExp(`id="${id}"`), `${row} row hosts #${id}`);
|
|
104
104
|
};
|
|
105
|
-
has('code', ['physExisting', 'c-green', 'c-adopt', 'adoptNote', '
|
|
105
|
+
has('code', ['physExisting', 'c-green', 'c-adopt', 'adoptNote', 'ghNoAccess', 'ghAdopt', 'ghFresh', 'manualHome', 'h-home']);
|
|
106
106
|
has('address', ['f-slug', 'addrConfirm', 'addrCustom', 'f-domain', 'f-noaddr']);
|
|
107
107
|
has('app', ['appOpts', 'renderLater', 'renderSetup', 'f-renderKey', 'renderWs', 'renderSvc', 'renderNew']);
|
|
108
108
|
has('extras', ['modQ', 'modCards', 'modWhy', 'modCoreNote']);
|