@bongos/core 1.19.663 → 1.19.665
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 +40 -25
- package/docs/copy-inventory.md +28 -17
- package/docs/copy-registry.json +121 -22
- package/docs/module-api-changelog.md +14 -0
- package/modules/hall-ui/public/hall-kit.js +36 -2
- package/modules/hall-ui/public/studio.css +121 -1
- package/modules/hall-ui/public/studio.html +5 -1
- package/modules/hall-ui/public/studio.js +337 -13
- package/modules/hall-ui/public/studio.states.json +1 -1
- package/modules/provisioning/credential-preflight.js +170 -0
- package/modules/provisioning/routes/provisioning.js +16 -11
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/hall-preview/server.js +11 -0
- package/scripts/hall-preview/task-visual.js +89 -0
- package/src/module-api.js +15 -1
- package/tests/hall_studio_world.mjs +435 -8
- package/tests/module_api.mjs +1 -0
- package/tests/provisioning_credential_preflight.mjs +153 -0
- package/tests/provisioning_onboard_mode.mjs +13 -0
- package/tests/rank_tier_single_source.mjs +8 -1
- package/tests/task_visuals.mjs +35 -0
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.665",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.665",
|
|
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.19.
|
|
3
|
+
"version": "1.19.665",
|
|
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",
|
|
@@ -183,6 +183,17 @@ app.use((req, res, next) => {
|
|
|
183
183
|
return;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
// The ship-time VISUAL is an image, not a fixture (task 1003829). Without
|
|
187
|
+
// this, every surface that renders one — the task record, a ship feed, the
|
|
188
|
+
// Studio's review card, where the picture IS the card — previews as a broken
|
|
189
|
+
// frame, so the one thing being reviewed is the one thing you cannot see.
|
|
190
|
+
// A generated stand-in; see scripts/hall-preview/task-visual.js.
|
|
191
|
+
if (/^task-visuals\/[A-Za-z0-9._-]+$/.test(apiPath) && req.method === 'GET') {
|
|
192
|
+
const png = require('./task-visual').placeholderPng();
|
|
193
|
+
res.set({ 'content-type': 'image/png', 'cache-control': 'no-store' });
|
|
194
|
+
return res.end(png);
|
|
195
|
+
}
|
|
196
|
+
|
|
186
197
|
if (req.method !== 'GET') {
|
|
187
198
|
return res.status(501).json({ error: { code: 'preview_readonly', message: 'the preview harness serves fixtures; writes are not wired' } });
|
|
188
199
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// scripts/hall-preview/task-visual.js — a stand-in ship-time visual for the
|
|
3
|
+
// preview harness (task 1003829).
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS EXISTS. Everything else in this harness is answered from a checked-in
|
|
6
|
+
// JSON fixture, but a ship-time visual is an IMAGE: the API stub answers
|
|
7
|
+
// /api/bongos/* with JSON, so a page that renders <img src="/api/bongos/
|
|
8
|
+
// task-visuals/task-1899-….png"> got a 404 body and a broken frame. The Studio's
|
|
9
|
+
// review card (task 1003829) makes that image the subject of the card, and a
|
|
10
|
+
// surface whose subject cannot be rendered cannot be reviewed — the whole point
|
|
11
|
+
// of this harness.
|
|
12
|
+
//
|
|
13
|
+
// It is a GENERATED placeholder, not a checked-in screenshot: a few flat bands
|
|
14
|
+
// at a real screenshot's aspect ratio, so a reviewer can judge the plate's size,
|
|
15
|
+
// fit, caption and crop without a binary in the repo. Deliberately obvious —
|
|
16
|
+
// nobody should mistake it for a real ship's visual.
|
|
17
|
+
//
|
|
18
|
+
// A ~40-line PNG encoder rather than a dependency, matching the kit's own
|
|
19
|
+
// png.js (a ~50-line DEcoder for the same reason). One IDAT, filter type 0.
|
|
20
|
+
const zlib = require('node:zlib');
|
|
21
|
+
|
|
22
|
+
function chunk(type, data) {
|
|
23
|
+
const len = Buffer.alloc(4);
|
|
24
|
+
len.writeUInt32BE(data.length);
|
|
25
|
+
const body = Buffer.concat([Buffer.from(type, 'ascii'), data]);
|
|
26
|
+
const crc = Buffer.alloc(4);
|
|
27
|
+
crc.writeUInt32BE(crc32(body) >>> 0);
|
|
28
|
+
return Buffer.concat([len, body, crc]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let CRC_TABLE = null;
|
|
32
|
+
function crc32(buf) {
|
|
33
|
+
if (!CRC_TABLE) {
|
|
34
|
+
CRC_TABLE = new Int32Array(256);
|
|
35
|
+
for (let n = 0; n < 256; n++) {
|
|
36
|
+
let c = n;
|
|
37
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
38
|
+
CRC_TABLE[n] = c;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let c = -1;
|
|
42
|
+
for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
|
43
|
+
return c ^ -1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// An RGB PNG from a per-pixel colour function. RGB (colour type 2), 8-bit,
|
|
47
|
+
// non-interlaced — the shape the kit's decoder also speaks.
|
|
48
|
+
function encodeRgb(w, h, at) {
|
|
49
|
+
const stride = w * 3;
|
|
50
|
+
const raw = Buffer.alloc((stride + 1) * h);
|
|
51
|
+
for (let y = 0; y < h; y++) {
|
|
52
|
+
const row = y * (stride + 1);
|
|
53
|
+
raw[row] = 0; // filter: none
|
|
54
|
+
for (let x = 0; x < w; x++) {
|
|
55
|
+
const [r, g, b] = at(x, y);
|
|
56
|
+
const p = row + 1 + x * 3;
|
|
57
|
+
raw[p] = r; raw[p + 1] = g; raw[p + 2] = b;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const ihdr = Buffer.alloc(13);
|
|
61
|
+
ihdr.writeUInt32BE(w, 0);
|
|
62
|
+
ihdr.writeUInt32BE(h, 4);
|
|
63
|
+
ihdr[8] = 8; ihdr[9] = 2; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
|
|
64
|
+
return Buffer.concat([
|
|
65
|
+
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
66
|
+
chunk('IHDR', ihdr),
|
|
67
|
+
chunk('IDAT', zlib.deflateSync(raw, { level: 9 })),
|
|
68
|
+
chunk('IEND', Buffer.alloc(0)),
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The stand-in: a wide plate of flat bands with a paler inset, so its edges,
|
|
73
|
+
// aspect ratio and any crop are all visible at a glance. Cached — the harness
|
|
74
|
+
// may serve it on every paint.
|
|
75
|
+
let CACHED = null;
|
|
76
|
+
function placeholderPng() {
|
|
77
|
+
if (CACHED) return CACHED;
|
|
78
|
+
const W = 1200, H = 700;
|
|
79
|
+
const BANDS = [[38, 34, 30], [74, 66, 58], [176, 96, 62], [214, 200, 184], [246, 243, 238]];
|
|
80
|
+
CACHED = encodeRgb(W, H, (x, y) => {
|
|
81
|
+
const inset = x > W * 0.08 && x < W * 0.92 && y > H * 0.14 && y < H * 0.86;
|
|
82
|
+
const band = BANDS[Math.min(BANDS.length - 1, Math.floor((y / H) * BANDS.length))];
|
|
83
|
+
if (!inset) return band;
|
|
84
|
+
return band.map((c) => Math.min(255, Math.round(c + (255 - c) * 0.42)));
|
|
85
|
+
});
|
|
86
|
+
return CACHED;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { placeholderPng, encodeRgb };
|
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.
|
|
74
|
+
const CORE_VERSION = '1.19.665'; // 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');
|
|
@@ -197,6 +197,20 @@ module.exports = {
|
|
|
197
197
|
// isConfigured/encrypt/decrypt/provisionMasterKey without reaching core.
|
|
198
198
|
get secretBox() { return require('./bongos/secret-box'); },
|
|
199
199
|
|
|
200
|
+
// --- grantedScopeCoversRepos(scope) — does a scope string GitHub ACTUALLY
|
|
201
|
+
// GRANTED still reach private repos? Exposed for the provisioning module's
|
|
202
|
+
// create-route credential preflight (task 1002909), which must tell "that
|
|
203
|
+
// repo does not exist" apart from "it is private and your pre-ADR-0155 token
|
|
204
|
+
// cannot see it" — GitHub answers 404 to both.
|
|
205
|
+
//
|
|
206
|
+
// Shared rather than re-implemented because the subtlety is a trap:
|
|
207
|
+
// `public_repo` CONTAINS the substring "repo", so a naive match accepts
|
|
208
|
+
// exactly the stale token this guards against. auth-config.js is KERNEL
|
|
209
|
+
// (ADR 0091 §1), so this stays inside the kernel-imports-only rule — unlike
|
|
210
|
+
// db.getGithubToken / github-repos, which are domain and which a module
|
|
211
|
+
// therefore reaches via the exposed `pool` and its own client instead.
|
|
212
|
+
get grantedScopeCoversRepos() { return require('./bongos/auth-config').grantedScopeCoversRepos; },
|
|
213
|
+
|
|
200
214
|
// --- KERNEL trust classifiers (ADR 0091 §1, KERNEL_FILES) — the deterministic
|
|
201
215
|
// route→rank + protected-path matchers. They are KERNEL (the trust
|
|
202
216
|
// boundary, never "called" by a domain), exposed so a module that runs the
|
|
@@ -28,15 +28,130 @@ const HTML = read('studio.html');
|
|
|
28
28
|
const HTML_NC = HTML.replace(/<!--[\s\S]*?-->/g, '');
|
|
29
29
|
const JS = read('studio.js');
|
|
30
30
|
const CSS = read('studio.css');
|
|
31
|
+
// The file's comments legitimately NAME routes and rules it must not call (the
|
|
32
|
+
// header explains what the room does not do, and the allowlist documents each
|
|
33
|
+
// entry), so the route invariants below scan the CODE. Comments only — string
|
|
34
|
+
// contents are untouched, which is where a real route would live.
|
|
35
|
+
const JS_NC = JS.replace(/^\s*\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// What the room may write — NARROWED, not dropped (task 1003829).
|
|
39
|
+
//
|
|
40
|
+
// This test used to assert studio.js issued NO write at all. Task 1003829
|
|
41
|
+
// relaxed that, deliberately and only this far: "The Studio's no-write posture
|
|
42
|
+
// is relaxed only for the artist's own verdict on their own queue — it still
|
|
43
|
+
// gates nothing and still holds no deploy." So the invariant becomes an
|
|
44
|
+
// ALLOWLIST. The thing goal 1000074 actually forbids — a live CMS, a room that
|
|
45
|
+
// edits a shipped string or asset — is what the second test below pins, and it
|
|
46
|
+
// is the half that must never be relaxed again.
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
test('the Studio writes ONLY the artist\'s verdict on their own review queue', () => {
|
|
50
|
+
// Each permitted write moves the REVIEW TASK (dismiss it, note on it, file
|
|
51
|
+
// its follow-up and close it against that). Every one of them is a route the
|
|
52
|
+
// board's ordinary triage already had; none of them touches the product.
|
|
53
|
+
const allowed = [
|
|
54
|
+
/^POST \$\{API\}\/tasks\/\$\{reviewId\}\/abandon$/, // looks good → dismissed
|
|
55
|
+
/^PATCH \$\{API\}\/tasks\/\$\{reviewId\}$/, // note it → the words, on the review
|
|
56
|
+
/^POST \$\{API\}\/tasks$/, // needs work→ the follow-up task
|
|
57
|
+
/^POST \$\{API\}\/tasks\/\$\{reviewId\}\/merge$/, // …and the review closed against it
|
|
58
|
+
];
|
|
59
|
+
// Every write site, as "<VERB> <route expression>" — the verb from the
|
|
60
|
+
// request call, the route from the WRITE_ROUTES entry it names.
|
|
61
|
+
const routes = new Map(
|
|
62
|
+
[...JS.matchAll(/^\s*(\w+):\s*\([^)]*\)\s*=>\s*`([^`]+)`,/gm)].map((m) => [m[1], m[2]])
|
|
63
|
+
);
|
|
64
|
+
// EVERY write site in the file, with the URL argument it was given — so a
|
|
65
|
+
// write cannot dodge the allowlist by naming its URL inline. That totality is
|
|
66
|
+
// what makes this test a wall and not a sample.
|
|
67
|
+
const sites = [...JS_NC.matchAll(/\.request\(\s*['"](POST|PATCH|PUT|DELETE)['"]\s*,\s*([^,)]+)/g)]
|
|
68
|
+
.map(([, verb, arg]) => {
|
|
69
|
+
const named = /^WRITE_ROUTES\.(\w+)\(/.exec(arg.trim());
|
|
70
|
+
return { verb, arg: arg.trim(), route: named ? routes.get(named[1]) : null };
|
|
71
|
+
});
|
|
72
|
+
assert.equal(sites.length, 4, `exactly the four verdict writes; found ${sites.length}`);
|
|
73
|
+
for (const s of sites) {
|
|
74
|
+
assert.ok(s.route, `every write names a WRITE_ROUTES entry, never an inline URL: ${s.verb} ${s.arg}`);
|
|
75
|
+
const site = `${s.verb} ${s.route}`;
|
|
76
|
+
assert.ok(allowed.some((re) => re.test(site)), `write outside the allowlist: ${site}`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('the Studio still edits no live string and no asset — the no-live-CMS rule', () => {
|
|
81
|
+
// The half that is NOT relaxed (criterion artist-edits-land-as-tasks). A copy
|
|
82
|
+
// change or an asset change reaches people only as a claimed, graded, shipped
|
|
83
|
+
// task, so this room must never call the routes that would write one directly.
|
|
84
|
+
const forbidden = [
|
|
85
|
+
/copy-desk\/proposals/, // wording — belongs to the copy desk, under a claim
|
|
86
|
+
/copy-desk\/flags\/[^'"`]*\/close/, // someone else's flag verdict — the copy desk's own act
|
|
87
|
+
/\/visual\b/, // POST/DELETE /tasks/:id/visual — the asset itself
|
|
88
|
+
/\/confirm\b/, /\/ship\b/, /\/grade\b/, /\/claims\b/, // the ship pipeline: it gates nothing
|
|
89
|
+
];
|
|
90
|
+
for (const re of forbidden) {
|
|
91
|
+
assert.ok(!re.test(JS_NC), `the Studio must not write ${re}`);
|
|
92
|
+
}
|
|
93
|
+
// "Needs work" files a TASK and says so: the follow-up is ordinary work that
|
|
94
|
+
// somebody claims, not an edit this room performs.
|
|
95
|
+
assert.match(JS, /never edits a live string or asset/, 'the follow-up brief says what the room did not do');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('the verdict is offered only to a viewer the routes will accept', () => {
|
|
99
|
+
// The task 1003751 lesson: a card must not print a verb its route refuses.
|
|
100
|
+
// All four verdict routes floor at metic (modules/government/catalog.js), so
|
|
101
|
+
// the weights are drawn only above that floor and everyone else is told where
|
|
102
|
+
// the verdict is recorded instead. Cosmetic — ADR 0016, the server enforces.
|
|
103
|
+
assert.match(JS, /RANK_ORDER\.metic/, 'the offer is gated on the routes\' own floor');
|
|
104
|
+
assert.match(JS, /function canVerdict\(\)/, 'and the gate is one named predicate');
|
|
105
|
+
assert.match(JS, /if \(!canVerdict\(\)\)/, 'read both by the renderer and by the handler');
|
|
106
|
+
assert.match(JS, /studio-verdict__sealed/, 'a viewer below the floor gets a sentence, not three dead buttons');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('the review card shows the reviewed work\'s picture, through the kit', () => {
|
|
110
|
+
// "the picture is the card" — and drawn by the kit's ONE full-size renderer
|
|
111
|
+
// rather than a third hand-rolled <figure> (the task's own instruction).
|
|
112
|
+
assert.match(JS, /kit\.taskVisualFigureHtml\(/, 'the plate comes from the kit renderer');
|
|
113
|
+
assert.ok(!/<figure/.test(JS), 'studio.js builds no <figure> of its own');
|
|
114
|
+
assert.match(HTML, /hall-kit\.js/, 'and the page loads the kit that owns it');
|
|
115
|
+
// The degrade: a review with no visual (or one that could not be read) draws
|
|
116
|
+
// nothing at all rather than an empty frame, so it still reads as a review.
|
|
117
|
+
assert.match(JS, /if \(!p\.shipped \|\| !kit/, 'no reviewed task, no kit → no plate');
|
|
118
|
+
// The reviewed task is read for the card in focus, once — not 50 times up front.
|
|
119
|
+
assert.match(JS, /p\.hydrated = true;/, 'the read is made once per review');
|
|
120
|
+
assert.match(JS, /\$\{API\}\/tasks\/\$\{encodeURIComponent\(p\.shippedId\)\}/, 'and it reads the SHIPPED task the review names');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('a security_sensitive task\'s visual is suppressed, as the server suppresses it', () => {
|
|
124
|
+
// modules/lifecycle/task-visuals.js isPubliclyViewable makes a
|
|
125
|
+
// security_sensitive task's image the one visual the platform does not
|
|
126
|
+
// re-publish; the public feed's SQL agrees. The room agrees too — it keeps the
|
|
127
|
+
// id and drops the rest, so the card degrades to the copy it always showed.
|
|
128
|
+
assert.match(JS, /security_sensitive === true \? \{ id: t\.id \}/, 'the row is stripped to its id when it is sensitive');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('a note APPENDS to a freshly-read brief — it can never blank one', () => {
|
|
132
|
+
// PATCH description REPLACES the field. Appending to a base that is stale, or
|
|
133
|
+
// that a narrowed list projection simply did not carry, would delete the
|
|
134
|
+
// cascade's brief instead of adding to it — so the base is re-read and the
|
|
135
|
+
// write is skipped entirely when that read fails.
|
|
136
|
+
assert.match(JS_NC, /const cur = await api\.request\('GET', `\$\{API\}\/tasks\/\$\{p\.taskId\}`\);/, 'the current brief is re-read');
|
|
137
|
+
assert.match(JS_NC, /if \(base === null\) \{/, 'and a failed re-read writes nothing');
|
|
138
|
+
assert.match(JS_NC, /description: `\$\{base\}\$\{block\}`/, 'the write is base + the note, in that order');
|
|
139
|
+
// Ordering, stated as code: the GET must precede the PATCH in the function.
|
|
140
|
+
const fn = /async function verdictNote[\s\S]*?\n \}/.exec(JS_NC)[0];
|
|
141
|
+
assert.ok(fn.indexOf("request('GET'") < fn.indexOf("request('PATCH'"), 'read before write');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('the three weights are the whole verdict, and two of them require words', () => {
|
|
145
|
+
// The room asks for nothing else (the task's DONE WHEN): one field, three
|
|
146
|
+
// answers. "looks good" may be wordless; a note with no note and a follow-up
|
|
147
|
+
// with no brief are refused inline rather than filed empty.
|
|
148
|
+
assert.match(JS, /const VERDICTS = \{ good: verdictGood, note: verdictNote, work: verdictWork \};/, 'exactly three weights');
|
|
149
|
+
assert.match(JS, /if \(!said\) return verdictError\('Write what you saw first/, 'a note needs words');
|
|
150
|
+
assert.match(JS, /if \(!said\) return verdictError\('Write what needs work first/, 'a follow-up needs a brief');
|
|
151
|
+
// The follow-up is idempotent on (source, source_ref), so a double-click
|
|
152
|
+
// resolves to the same task instead of filing two.
|
|
153
|
+
assert.match(JS, /source_ref: `studio-needs-work-on-review-\$\{p\.taskId\}`/, 'the follow-up is keyed for idempotency');
|
|
154
|
+
assert.match(JS, /let busy = false;/, 'and one verdict runs at a time');
|
|
40
155
|
});
|
|
41
156
|
|
|
42
157
|
test('the reviews read is bounded and correct', () => {
|
|
@@ -88,6 +203,318 @@ test('the accent is spent only where the rule allows', () => {
|
|
|
88
203
|
assert.ok(accentFills.length <= 1, `studio.css spends the accent on at most the lamp; found ${accentFills.length}`);
|
|
89
204
|
});
|
|
90
205
|
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Boot the REAL studio.js under a DOM stub, with the REAL hall-kit.js beside it
|
|
208
|
+
// (the bootShell pattern below, pointed at the page script). The static-source
|
|
209
|
+
// assertions above pin the wiring; these render the card and read what it
|
|
210
|
+
// actually says, which is the only way to pin a DEGRADE — "no picture" and
|
|
211
|
+
// "picture suppressed" both have to produce a review that still reads as one.
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// `onWrite` decides what each non-GET answers, so a test can drive the happy
|
|
214
|
+
// path AND the refusals. Default: every write succeeds, and POST /tasks mints
|
|
215
|
+
// NEW_TASK_ID so the "needs work" pair can be followed to its merge.
|
|
216
|
+
const NEW_TASK_ID = '2001';
|
|
217
|
+
async function bootStudio({ reviews = [], shipped = {}, rank = 'metic', flags = null, onWrite = null } = {}) {
|
|
218
|
+
// The REAL kit, not a stub — the plate's URL guard and escaping are its code,
|
|
219
|
+
// and a stub here would make the degrade tests below pass for the wrong reason
|
|
220
|
+
// (no kit also means no plate). It is node-require-safe by its own contract.
|
|
221
|
+
const kit = (await import(`file://${path.join(HALL, 'hall-kit.js').replace(/\\/g, '/')}`)).default;
|
|
222
|
+
assert.equal(typeof kit.taskVisualFigureHtml, 'function', 'the real kit loaded');
|
|
223
|
+
const els = new Map();
|
|
224
|
+
const el = (id) => {
|
|
225
|
+
if (!els.has(id)) els.set(id, { id, innerHTML: '', textContent: '', hidden: false, value: '' });
|
|
226
|
+
return els.get(id);
|
|
227
|
+
};
|
|
228
|
+
const calls = [];
|
|
229
|
+
const request = async (method, url, body) => {
|
|
230
|
+
calls.push({ method, url, body });
|
|
231
|
+
if (method !== 'GET') {
|
|
232
|
+
const custom = onWrite && onWrite({ method, url, body });
|
|
233
|
+
if (custom) return custom;
|
|
234
|
+
if (/\/tasks$/.test(url)) return { ok: true, data: { task: { id: NEW_TASK_ID } } };
|
|
235
|
+
return { ok: true, data: { ok: true } };
|
|
236
|
+
}
|
|
237
|
+
if (/\/me$/.test(url)) return { ok: true, data: { builder: { display_name: 'Artist A', rank } } };
|
|
238
|
+
if (/\/tasks\?discipline=artist/.test(url)) return { ok: true, data: { tasks: reviews } };
|
|
239
|
+
if (/\/copy-desk\/queue/.test(url)) return flags ? { ok: true, data: { flags } } : { ok: false, data: {} };
|
|
240
|
+
// GET /tasks/:id answers for BOTH kinds of row the room reads by id: the
|
|
241
|
+
// reviewed (shipped) task the card's picture comes from, and the review
|
|
242
|
+
// itself, which "note it" re-reads before appending. `shipped` is the
|
|
243
|
+
// per-test control for the first; the queue rows serve the second, as the
|
|
244
|
+
// real route would. An id in neither is a 404, which is its own test.
|
|
245
|
+
const m = /\/tasks\/(\d+)$/.exec(url);
|
|
246
|
+
if (m && shipped[m[1]]) return { ok: true, data: { task: shipped[m[1]] } };
|
|
247
|
+
const review = m && reviews.find((r) => String(r.id) === m[1]);
|
|
248
|
+
if (review) return { ok: true, data: { task: review } };
|
|
249
|
+
return { ok: false, data: {} };
|
|
250
|
+
};
|
|
251
|
+
const handlers = [];
|
|
252
|
+
const window = {
|
|
253
|
+
BongosClient: { createClient: () => ({ request }) },
|
|
254
|
+
OTB: {
|
|
255
|
+
escapeHtml: (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])),
|
|
256
|
+
$: (sel) => el(String(sel).replace(/^#/, '')),
|
|
257
|
+
toast: () => {},
|
|
258
|
+
errMessage: (d) => (d && d.error && d.error.message) || '',
|
|
259
|
+
},
|
|
260
|
+
OTBKit: kit,
|
|
261
|
+
scrollTo: () => {},
|
|
262
|
+
};
|
|
263
|
+
const document = { addEventListener: (t, h) => handlers.push({ t, h }) };
|
|
264
|
+
const sandbox = { window, document, console, setTimeout, clearTimeout, Date, encodeURIComponent };
|
|
265
|
+
sandbox.globalThis = sandbox;
|
|
266
|
+
vm.createContext(sandbox);
|
|
267
|
+
vm.runInContext(JS, sandbox, { filename: 'studio.js' });
|
|
268
|
+
const settle = async (n = 8) => { for (let i = 0; i < n; i++) await new Promise((r) => setImmediate(r)); };
|
|
269
|
+
// The boot IIFE awaits /me, then the two queue reads, then paint() fires the
|
|
270
|
+
// lazy reviewed-task read which repaints.
|
|
271
|
+
await settle();
|
|
272
|
+
// Fire the page's OWN delegated click handler with the element it looks for,
|
|
273
|
+
// so the verdict flows are exercised rather than read. `say` fills the words
|
|
274
|
+
// field first, exactly as a person would.
|
|
275
|
+
const fire = async (attr, value, { say = null } = {}) => {
|
|
276
|
+
if (say !== null) el('studio-words').value = say;
|
|
277
|
+
const node = { dataset: { [attr]: value } };
|
|
278
|
+
const ev = { target: { closest: (sel) => (sel === `[data-${attr}]` ? node : null) } };
|
|
279
|
+
for (const { t, h } of handlers) if (t === 'click') h(ev);
|
|
280
|
+
await settle();
|
|
281
|
+
};
|
|
282
|
+
return {
|
|
283
|
+
focus: () => el('studio-focus').innerHTML,
|
|
284
|
+
err: () => ({ text: el('studio-verdict-err').textContent, hidden: el('studio-verdict-err').hidden }),
|
|
285
|
+
field: () => el('studio-words').value,
|
|
286
|
+
writes: () => calls.filter((c) => c.method !== 'GET'),
|
|
287
|
+
calls,
|
|
288
|
+
verdict: (kind, opts) => fire('verdict', kind, opts),
|
|
289
|
+
skip: () => fire('skip', ''),
|
|
290
|
+
settle,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const REVIEW = {
|
|
295
|
+
id: '1900',
|
|
296
|
+
title: 'Artist review: the copy and visuals that shipped in task 1899',
|
|
297
|
+
description: 'STEP 1 — coach the grader.',
|
|
298
|
+
status: 'backlog',
|
|
299
|
+
source: 'cascade',
|
|
300
|
+
source_ref: 'cascade-artist-review-on-ship-1899',
|
|
301
|
+
goal_id: '1000074',
|
|
302
|
+
version_id: 'BONGOS-V2',
|
|
303
|
+
created_at: '2026-09-04T12:30:00.000Z',
|
|
304
|
+
};
|
|
305
|
+
const VISUAL_URL = '/api/bongos/task-visuals/task-1899-0011223344556677.png';
|
|
306
|
+
|
|
307
|
+
test('a review WITH a visual shows the picture, and folds the steps behind the door', async () => {
|
|
308
|
+
const { focus, calls } = await bootStudio({
|
|
309
|
+
reviews: [REVIEW],
|
|
310
|
+
shipped: { 1899: { id: '1899', visual_url: VISUAL_URL, visual_alt: 'The new hero, dark mode', security_sensitive: false } },
|
|
311
|
+
});
|
|
312
|
+
const html = focus();
|
|
313
|
+
assert.match(html, /class="studio-plate"/, 'the plate is drawn');
|
|
314
|
+
assert.match(html, /src="\/api\/bongos\/task-visuals\/task-1899-0011223344556677\.png"/, 'from the reviewed task\'s own visual');
|
|
315
|
+
assert.match(html, /studio-plate__caption">The new hero, dark mode/, 'with visual_alt as the caption');
|
|
316
|
+
// With a picture on it the card is the picture, one line of framing, and the
|
|
317
|
+
// verdict — the steps describe what happens after the door, not before it.
|
|
318
|
+
assert.ok(!/studio-steps/.test(html), 'the two steps are not between the artist and their answer');
|
|
319
|
+
assert.match(html, /Say what you see/, 'one short line of framing instead');
|
|
320
|
+
assert.match(html, /data-verdict="good"/, 'and the three weights are there');
|
|
321
|
+
// ONE extra read for the card in focus, not one per queued review.
|
|
322
|
+
const reads = calls.filter((c) => /\/tasks\/\d+$/.test(c.url));
|
|
323
|
+
assert.equal(reads.length, 1, `one reviewed-task read; made ${reads.length}`);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('a review with NO visual degrades to the copy it always showed', async () => {
|
|
327
|
+
const { focus } = await bootStudio({
|
|
328
|
+
reviews: [REVIEW],
|
|
329
|
+
shipped: { 1899: { id: '1899', visual_url: null, security_sensitive: false } },
|
|
330
|
+
});
|
|
331
|
+
const html = focus();
|
|
332
|
+
assert.ok(!/studio-plate/.test(html), 'no empty frame');
|
|
333
|
+
assert.ok(!/<img/.test(html), 'and no image at all');
|
|
334
|
+
assert.match(html, /studio-steps/, 'both craft steps are back');
|
|
335
|
+
assert.match(html, /coaches the grader on how it judged the work/, 'as is the full framing paragraph');
|
|
336
|
+
assert.match(html, /data-verdict="good"/, 'the verdict is still offered — this is still a review');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('a security_sensitive reviewed task shows no picture, and still reads as a review', async () => {
|
|
340
|
+
const { focus } = await bootStudio({
|
|
341
|
+
reviews: [REVIEW],
|
|
342
|
+
shipped: { 1899: { id: '1899', visual_url: VISUAL_URL, visual_alt: 'the exploit', security_sensitive: true } },
|
|
343
|
+
});
|
|
344
|
+
const html = focus();
|
|
345
|
+
assert.ok(!/task-visuals/.test(html), 'the URL never reaches the page');
|
|
346
|
+
assert.ok(!/the exploit/.test(html), 'nor does the alt text');
|
|
347
|
+
assert.match(html, /studio-steps/, 'it degrades to the copy, exactly like a review with no visual');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('a reviewed task that cannot be read leaves a working review card', async () => {
|
|
351
|
+
// A 404/403 on the reviewed task (it was merged, or the viewer cannot see it)
|
|
352
|
+
// must cost the artist nothing: the card is the one it always was.
|
|
353
|
+
const { focus } = await bootStudio({ reviews: [REVIEW], shipped: {} });
|
|
354
|
+
const html = focus();
|
|
355
|
+
assert.ok(!/studio-plate/.test(html));
|
|
356
|
+
assert.match(html, /studio-steps/);
|
|
357
|
+
assert.match(html, /The copy and visuals that shipped in task #1899/);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('below the routes\' floor the card offers a sentence, not three refusals', async () => {
|
|
361
|
+
const { focus } = await bootStudio({
|
|
362
|
+
reviews: [REVIEW],
|
|
363
|
+
shipped: { 1899: { id: '1899', visual_url: VISUAL_URL, visual_alt: 'a', security_sensitive: false } },
|
|
364
|
+
rank: 'xenos',
|
|
365
|
+
});
|
|
366
|
+
const html = focus();
|
|
367
|
+
assert.ok(!/data-verdict=/.test(html), 'no weight is drawn');
|
|
368
|
+
assert.match(html, /studio-verdict__sealed/, 'the sentence is');
|
|
369
|
+
assert.match(html, /open it to dismiss it, note what you saw, or file the follow-up/);
|
|
370
|
+
// The picture is NOT rank-gated — looking is every craft's business.
|
|
371
|
+
assert.match(html, /studio-plate/, 'and the artist can still see the work');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
// The verdict flows, FIRED rather than read. The static assertions above pin
|
|
376
|
+
// the wiring and the allowlist; these click the page's own delegated handler
|
|
377
|
+
// and inspect what it actually sent and what state it left behind — the splice
|
|
378
|
+
// math in retire(), the double-submit guard, and the exact request bodies.
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
const SHIPPED_WITH_VISUAL = { 1899: { id: '1899', visual_url: VISUAL_URL, visual_alt: 'The new hero', security_sensitive: false } };
|
|
381
|
+
const three = () => [
|
|
382
|
+
{ ...REVIEW, id: '1900', source_ref: 'cascade-artist-review-on-ship-1899' },
|
|
383
|
+
{ ...REVIEW, id: '1901', source_ref: 'cascade-artist-review-on-ship-1898', created_at: '2026-09-03T00:00:00.000Z' },
|
|
384
|
+
{ ...REVIEW, id: '1902', source_ref: 'cascade-artist-review-on-ship-1897', created_at: '2026-09-02T00:00:00.000Z' },
|
|
385
|
+
];
|
|
386
|
+
|
|
387
|
+
test('FIRE looks good: one abandon, carrying who said so, and the stone is gone', async () => {
|
|
388
|
+
const s = await bootStudio({ reviews: three(), shipped: SHIPPED_WITH_VISUAL });
|
|
389
|
+
assert.match(s.focus(), /1 of 3/);
|
|
390
|
+
await s.verdict('good', { say: 'The hero lands.' });
|
|
391
|
+
const w = s.writes();
|
|
392
|
+
assert.equal(w.length, 1, `exactly one write; got ${JSON.stringify(w.map((c) => c.method + ' ' + c.url))}`);
|
|
393
|
+
assert.match(w[0].url, /\/tasks\/1900\/abandon$/, 'the REVIEW is abandoned, not the reviewed task');
|
|
394
|
+
assert.match(w[0].body.reason, /Reviewed in the studio by Artist A/, 'the reason names who judged it');
|
|
395
|
+
assert.match(w[0].body.reason, /The hero lands\./, 'and carries the optional words');
|
|
396
|
+
// retire() dropped it and the count re-read: 3 → 2, and the NEXT review is in
|
|
397
|
+
// focus rather than the list renumbering around a hole.
|
|
398
|
+
assert.match(s.focus(), /1 of 2/, 'the garden shrank and did not skip');
|
|
399
|
+
assert.match(s.focus(), /task #1898/, 'the next stone is the one that was behind it');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test('FIRE looks good with no words: still one abandon, no empty-words refusal', async () => {
|
|
403
|
+
const s = await bootStudio({ reviews: three(), shipped: SHIPPED_WITH_VISUAL });
|
|
404
|
+
await s.verdict('good');
|
|
405
|
+
assert.equal(s.writes().length, 1, '"looks good" is the one weight that needs no words');
|
|
406
|
+
assert.ok(!/undefined|null/.test(s.writes()[0].body.reason), 'and says nothing about absent words');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test('FIRE note it: the brief is re-read, appended, and the review STAYS in the garden', async () => {
|
|
410
|
+
const s = await bootStudio({ reviews: three(), shipped: SHIPPED_WITH_VISUAL });
|
|
411
|
+
await s.verdict('note', { say: 'The sub-line reads like a brochure.' });
|
|
412
|
+
const w = s.writes();
|
|
413
|
+
assert.equal(w.length, 1);
|
|
414
|
+
assert.equal(w[0].method, 'PATCH');
|
|
415
|
+
assert.match(w[0].url, /\/tasks\/1900$/);
|
|
416
|
+
// The cascade's own brief survives, with the note after it — the failure mode
|
|
417
|
+
// here is a PATCH that REPLACES the body, so assert both halves are present.
|
|
418
|
+
assert.match(w[0].body.description, /^STEP 1 — coach the grader\./, 'the original brief is still the start of the field');
|
|
419
|
+
assert.match(w[0].body.description, /ARTIST NOTE \(\d{4}-\d{2}-\d{2}, Artist A, from the studio\)/, 'the note is stamped and attributed');
|
|
420
|
+
assert.match(w[0].body.description, /The sub-line reads like a brochure\.$/, 'and ends with what was written');
|
|
421
|
+
// A note is not a dismissal.
|
|
422
|
+
assert.match(s.focus(), /1 of 3/, 'the review is still waiting');
|
|
423
|
+
assert.equal(s.field(), '', 'the field is cleared for the next thought');
|
|
424
|
+
assert.equal(s.err().hidden, true);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test('FIRE note it with no words: nothing is sent at all', async () => {
|
|
428
|
+
const s = await bootStudio({ reviews: three(), shipped: SHIPPED_WITH_VISUAL });
|
|
429
|
+
await s.verdict('note', { say: ' ' });
|
|
430
|
+
assert.deepEqual(s.writes(), [], 'no write');
|
|
431
|
+
assert.deepEqual(s.calls.filter((c) => /\/tasks\/1900$/.test(c.url)), [], 'and not even the re-read');
|
|
432
|
+
assert.match(s.err().text, /Write what you saw first/);
|
|
433
|
+
assert.equal(s.err().hidden, false);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test('FIRE needs work: a task in the reviewed work\'s goal, then the review closed against it', async () => {
|
|
437
|
+
const s = await bootStudio({ reviews: three(), shipped: SHIPPED_WITH_VISUAL });
|
|
438
|
+
await s.verdict('work', { say: 'The plate is muddy at 320.' });
|
|
439
|
+
const w = s.writes();
|
|
440
|
+
assert.equal(w.length, 2, 'the pair: create, then merge');
|
|
441
|
+
const [create, merge] = w;
|
|
442
|
+
assert.match(create.url, /\/tasks$/);
|
|
443
|
+
assert.equal(create.body.goal_id, 1000074, 'the follow-up lands in the reviewed work\'s goal, as a number');
|
|
444
|
+
assert.equal(create.body.version_id, 'BONGOS-V2');
|
|
445
|
+
assert.equal(create.body.discipline, 'artist');
|
|
446
|
+
assert.equal(create.body.source_ref, 'studio-needs-work-on-review-1900', 'keyed so a double-click cannot file twice');
|
|
447
|
+
assert.match(create.body.description, /The plate is muddy at 320\./, 'the artist\'s words are the brief');
|
|
448
|
+
assert.match(create.body.description, /never edits a live string or asset/, 'and the brief says this is ordinary work');
|
|
449
|
+
assert.match(merge.url, /\/tasks\/1900\/merge$/, 'the REVIEW is the one closed');
|
|
450
|
+
assert.equal(merge.body.into_task_id, Number(NEW_TASK_ID), 'against the task just filed');
|
|
451
|
+
assert.match(s.focus(), /1 of 2/, 'and the review left the garden');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test('FIRE needs work when the merge fails: the filed task is NAMED, not lost', async () => {
|
|
455
|
+
// The half-done case is the one that matters: the follow-up exists, so the
|
|
456
|
+
// artist must be told its id rather than left thinking nothing happened.
|
|
457
|
+
const s = await bootStudio({
|
|
458
|
+
reviews: three(),
|
|
459
|
+
shipped: SHIPPED_WITH_VISUAL,
|
|
460
|
+
onWrite: ({ url }) => (/\/merge$/.test(url) ? { ok: false, data: { error: { code: 'x', message: 'no' } } } : null),
|
|
461
|
+
});
|
|
462
|
+
await s.verdict('work', { say: 'Muddy.' });
|
|
463
|
+
assert.equal(s.writes().length, 2, 'both halves were attempted');
|
|
464
|
+
assert.match(s.err().text, new RegExp(`Task ${NEW_TASK_ID} was filed`), 'the id is in the message');
|
|
465
|
+
assert.match(s.focus(), /1 of 3/, 'and the review stays put rather than vanishing unclosed');
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test('FIRE a refused abandon: the review stays, and the server\'s own sentence is shown', async () => {
|
|
469
|
+
const s = await bootStudio({
|
|
470
|
+
reviews: three(),
|
|
471
|
+
shipped: SHIPPED_WITH_VISUAL,
|
|
472
|
+
onWrite: () => ({ ok: false, data: { error: { code: 'rank_forbidden', message: 'metic or above' } } }),
|
|
473
|
+
});
|
|
474
|
+
await s.verdict('good');
|
|
475
|
+
assert.equal(s.err().text, 'metic or above', 'the route\'s message, not a generic one');
|
|
476
|
+
assert.match(s.focus(), /1 of 3/, 'nothing was retired on a refusal');
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test('FIRE two clicks: the second is swallowed, so a double-click files once', async () => {
|
|
480
|
+
// The busy flag, exercised: the write is held open, both clicks land, and only
|
|
481
|
+
// one request is made. Without the guard "needs work" files two tasks.
|
|
482
|
+
let release;
|
|
483
|
+
const held = new Promise((r) => { release = r; });
|
|
484
|
+
const s = await bootStudio({
|
|
485
|
+
reviews: three(),
|
|
486
|
+
shipped: SHIPPED_WITH_VISUAL,
|
|
487
|
+
onWrite: async () => { await held; return { ok: true, data: { ok: true } }; },
|
|
488
|
+
});
|
|
489
|
+
const first = s.verdict('good');
|
|
490
|
+
await s.verdict('good');
|
|
491
|
+
assert.equal(s.writes().length, 1, 'the second click did not start a second write');
|
|
492
|
+
release({ ok: true, data: { ok: true } });
|
|
493
|
+
await first;
|
|
494
|
+
await s.settle();
|
|
495
|
+
assert.equal(s.writes().length, 1);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test('FIRE below the floor: a forged click writes nothing', async () => {
|
|
499
|
+
// The weights are not drawn for a xenos — but the handler is delegated, so
|
|
500
|
+
// canVerdict() is re-checked in runVerdict rather than trusted to the render.
|
|
501
|
+
const s = await bootStudio({ reviews: three(), shipped: SHIPPED_WITH_VISUAL, rank: 'xenos' });
|
|
502
|
+
await s.verdict('good', { say: 'fine' });
|
|
503
|
+
await s.verdict('work', { say: 'not fine' });
|
|
504
|
+
assert.deepEqual(s.writes(), [], 'the room sends nothing it was not allowed to offer');
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test('FIRE skip: it advances and writes nothing, and the last skip ends quietly', async () => {
|
|
508
|
+
const s = await bootStudio({ reviews: three(), shipped: SHIPPED_WITH_VISUAL });
|
|
509
|
+
await s.skip();
|
|
510
|
+
assert.match(s.focus(), /2 of 3/);
|
|
511
|
+
await s.skip();
|
|
512
|
+
await s.skip();
|
|
513
|
+
assert.deepEqual(s.writes(), [], 'skipping is not a verdict');
|
|
514
|
+
// Nothing was tended, so the emptied room reads as quiet, not as finished.
|
|
515
|
+
assert.match(s.focus(), /Nothing waits right now|tended everything/);
|
|
516
|
+
});
|
|
517
|
+
|
|
91
518
|
// Boot the real shell.js under a DOM stub (the hall_nav.mjs pattern) whose
|
|
92
519
|
// #nav-studio records classList toggles, so applyCraftEmphasis is tested for
|
|
93
520
|
// what it DOES, not just that it exists.
|
package/tests/module_api.mjs
CHANGED
|
@@ -24,6 +24,7 @@ const PUBLISHED_SURFACE = [
|
|
|
24
24
|
'requirePermission', // BV1.R105: the authority-atom gate module routes migrate onto (1.19.0; ADR 0151)
|
|
25
25
|
'pool', 'getPool', 'instanceDbName', 'withTx', 'getBuilderById',
|
|
26
26
|
'secretBox', // Gemini-key storage relocated to modules/art-pipeline + its 'art.getBuilderGeminiKeyRow' port (ADR 0102)
|
|
27
|
+
'grantedScopeCoversRepos', // the create-route credential preflight's scope read (task 1002909); kernel (auth-config.js), unlike db/github-repos which the module reaches via `pool` + its own client
|
|
27
28
|
'accountExistenceReadRateLimit', // task 1003339 (ADR 0209): the shared per-IP budget both account-existence probes spend from — one instance, not a factory, so the budget is genuinely shared
|
|
28
29
|
'publicProjectFeedRateLimit', // task 1002327 (ADR 0252 §5.3): the per-IP ceiling on GET /projects/featured — the platform's most expensive anonymous read (a cold call fans out to up to 500 third-party origins). One instance for the same reason as its neighbour
|
|
29
30
|
'routeRankCheck', 'permissionPathCheck', // BV1.R73: kernel trust classifiers (1.6.0)
|